cli: change the module name of the cli (#1431)

This commit is contained in:
eball
2025-06-11 23:06:24 +08:00
committed by GitHub
parent f9072c9312
commit d484e41bbd
301 changed files with 6680 additions and 1059 deletions

View File

@@ -0,0 +1,172 @@
# `crypto` <!-- omit in toc -->
This package mostly exists to maintain parity with the structure of other web5 SDKs maintainted by TBD. Check out the [dsa](./dsa) package for supported Digital Signature Algorithms
> [!NOTE]
> If the need arises, this package will also contain cryptographic primitives for encryption
# Table of Contents <!-- omit in toc -->
- [Features](#features)
- [Usage](#usage)
- [`dsa`](#dsa)
- [Key Generation](#key-generation)
- [Signing](#signing)
- [Verifying](#verifying)
- [Directory Structure](#directory-structure)
- [Rationale](#rationale)
# Features
* 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](https://github.com/beclab/Olares/cli/pkg/web5/blob/5d50ce8f24e4b47b0a8626724e8a571e9b5c847f/crypto/dsa/dsa.go#L11-L14) that can be passed to the `GenerateKey` function e.g.
```go
package main
import (
"fmt"
"github.com/beclab/Olares/cli/pkg/web5/crypto/dsa"
)
func main() {
privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDSECP256K1)
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.
```go
package main
import (
"fmt"
"github.com/beclab/Olares/cli/pkg/web5/crypto/dsa"
)
func main() {
// Generate private key
privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDSECP256K1)
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.
```go
package main
import (
"fmt"
"github.com/beclab/Olares/cli/pkg/web5/crypto/dsa"
)
func main() {
// Generate ED25519 private key
privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDED25519)
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` ?_
* 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`

View File

@@ -0,0 +1,6 @@
// Package crypto provides the following functionality:
// * Key Generation: secp256k1, ed25519
// * Signing: secp256k1, ed25519
// * Verification: secp256k1, ed25519
// * A KeyManager abstraction that can be leveraged to manage/use keys (create, sign etc) as desired per the given use case
package crypto

View File

@@ -0,0 +1,108 @@
package dsa
import (
"fmt"
"olares-cli/pkg/web5/crypto/dsa/ecdsa"
"olares-cli/pkg/web5/crypto/dsa/eddsa"
"olares-cli/pkg/web5/jwk"
)
const (
AlgorithmIDSECP256K1 = ecdsa.SECP256K1AlgorithmID
AlgorithmIDED25519 = eddsa.ED25519AlgorithmID
)
// GeneratePrivateKey generates a private key using the algorithm specified by algorithmID.
func GeneratePrivateKey(algorithmID string) (jwk.JWK, error) {
if ecdsa.SupportsAlgorithmID(algorithmID) {
return ecdsa.GeneratePrivateKey(algorithmID)
} else if eddsa.SupportsAlgorithmID(algorithmID) {
return eddsa.GeneratePrivateKey(algorithmID)
}
return jwk.JWK{}, fmt.Errorf("unsupported algorithm: %s", algorithmID)
}
// GetPublicKey returns the public key corresponding to the given private key.
func GetPublicKey(privateKey jwk.JWK) jwk.JWK {
switch privateKey.KTY {
case ecdsa.KeyType:
return ecdsa.GetPublicKey(privateKey)
case eddsa.KeyType:
return eddsa.GetPublicKey(privateKey)
default:
return jwk.JWK{}
}
}
// Sign signs the payload using the given private key.
func Sign(payload []byte, jwk jwk.JWK) ([]byte, error) {
switch jwk.KTY {
case ecdsa.KeyType:
return ecdsa.Sign(payload, jwk)
case eddsa.KeyType:
return eddsa.Sign(payload, jwk)
default:
return nil, fmt.Errorf("unsupported key type: %s", jwk.KTY)
}
}
// Verify verifies the signature of the payload using the given public key.
func Verify(payload []byte, signature []byte, jwk jwk.JWK) (bool, error) {
switch jwk.KTY {
case ecdsa.KeyType:
return ecdsa.Verify(payload, signature, jwk)
case eddsa.KeyType:
return eddsa.Verify(payload, signature, jwk)
default:
return false, fmt.Errorf("unsupported key type: %s", jwk.KTY)
}
}
// GetJWA returns the JWA (JSON Web Algorithm) algorithm corresponding to the given key.
func GetJWA(jwk jwk.JWK) (string, error) {
switch jwk.KTY {
case ecdsa.KeyType:
return ecdsa.GetJWA(jwk)
case eddsa.KeyType:
return eddsa.GetJWA(jwk)
default:
return "", fmt.Errorf("unsupported key type: %s", jwk.KTY)
}
}
// BytesToPublicKey converts the given bytes to a public key based on the algorithm specified by algorithmID.
func BytesToPublicKey(algorithmID string, input []byte) (jwk.JWK, error) {
if ecdsa.SupportsAlgorithmID(algorithmID) {
return ecdsa.BytesToPublicKey(algorithmID, input)
} else if eddsa.SupportsAlgorithmID(algorithmID) {
return eddsa.BytesToPublicKey(algorithmID, input)
}
return jwk.JWK{}, fmt.Errorf("unsupported algorithm: %s", algorithmID)
}
// PublicKeyToBytes converts the provided public key to bytes
func PublicKeyToBytes(publicKey jwk.JWK) ([]byte, error) {
switch publicKey.KTY {
case ecdsa.KeyType:
return ecdsa.PublicKeyToBytes(publicKey)
case eddsa.KeyType:
return eddsa.PublicKeyToBytes(publicKey)
default:
return nil, fmt.Errorf("unsupported key type: %s", publicKey.KTY)
}
}
// AlgorithmID returns the algorithm ID for the given jwk.JWK
func AlgorithmID(jwk *jwk.JWK) (string, error) {
switch jwk.KTY {
case ecdsa.KeyType:
return ecdsa.AlgorithmID(jwk)
case eddsa.KeyType:
return eddsa.AlgorithmID(jwk)
default:
return "", fmt.Errorf("unsupported key type: %s", jwk.KTY)
}
}

View File

@@ -0,0 +1,150 @@
package dsa_test
import (
"encoding/hex"
"testing"
"olares-cli/pkg/web5/crypto/dsa"
"olares-cli/pkg/web5/crypto/dsa/ecdsa"
"olares-cli/pkg/web5/crypto/dsa/eddsa"
"olares-cli/pkg/web5/jwk"
"github.com/alecthomas/assert/v2"
)
func TestGeneratePrivateKeySECP256K1(t *testing.T) {
privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDSECP256K1)
assert.NoError(t, err)
assert.Equal[string](t, ecdsa.SECP256K1JWACurve, privateJwk.CRV)
assert.Equal[string](t, ecdsa.KeyType, privateJwk.KTY)
assert.True(t, privateJwk.D != "", "privateJwk.D is empty")
assert.True(t, privateJwk.X != "", "privateJwk.X is empty")
assert.True(t, privateJwk.Y != "", "privateJwk.Y is empty")
}
func TestGeneratePrivateKeyED25519(t *testing.T) {
privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDED25519)
if err != nil {
t.Errorf("failed to generate private key: %v", err.Error())
}
assert.NoError(t, err)
assert.Equal[string](t, eddsa.ED25519JWACurve, privateJwk.CRV)
assert.Equal[string](t, eddsa.KeyType, privateJwk.KTY)
assert.True(t, privateJwk.D != "", "privateJwk.D is empty")
assert.True(t, privateJwk.X != "", "privateJwk.X is empty")
}
func TestSignSECP256K1(t *testing.T) {
privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDSECP256K1)
assert.NoError(t, err)
payload := []byte("hello world")
signature, err := dsa.Sign(payload, privateJwk)
assert.NoError(t, err)
assert.True(t, len(signature) == 64, "invalid signature length")
}
func TestSignED25519(t *testing.T) {
privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDED25519)
assert.NoError(t, err)
payload := []byte("hello world")
signature, err := dsa.Sign(payload, privateJwk)
assert.NoError(t, err)
assert.True(t, len(signature) == 64, "invalid signature length")
}
func TestSignDeterministicSECP256K1(t *testing.T) {
privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDSECP256K1)
assert.NoError(t, err)
payload := []byte("hello world")
signature1, err := dsa.Sign(payload, privateJwk)
assert.NoError(t, err, "failed to sign")
signature2, err := dsa.Sign(payload, privateJwk)
assert.NoError(t, err)
assert.Equal(t, signature1, signature2, "signature is not deterministic")
}
func TestSignDeterministicED25519(t *testing.T) {
privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDED25519)
assert.NoError(t, err)
payload := []byte("hello world")
signature1, err := dsa.Sign(payload, privateJwk)
assert.NoError(t, err, "failed to sign")
signature2, err := dsa.Sign(payload, privateJwk)
assert.NoError(t, err)
assert.Equal(t, signature1, signature2, "signature is not deterministic")
}
func TestVerifySECP256K1(t *testing.T) {
privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDSECP256K1)
assert.NoError(t, err)
payload := []byte("hello world")
signature, err := dsa.Sign(payload, privateJwk)
assert.NoError(t, err)
publicJwk := dsa.GetPublicKey(privateJwk)
legit, err := dsa.Verify(payload, signature, publicJwk)
assert.NoError(t, err)
assert.True(t, legit, "failed to verify signature")
}
func TestVerifyED25519(t *testing.T) {
privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDED25519)
assert.NoError(t, err)
payload := []byte("hello world")
signature, err := dsa.Sign(payload, privateJwk)
assert.NoError(t, err)
publicJwk := dsa.GetPublicKey(privateJwk)
legit, err := dsa.Verify(payload, signature, publicJwk)
assert.NoError(t, err)
assert.True(t, legit, "failed to verify signature")
}
func TestBytesToPublicKey_BadAlgorithm(t *testing.T) {
_, err := dsa.BytesToPublicKey("yolocrypto", []byte{0x00, 0x01, 0x02, 0x03})
assert.Error(t, err)
}
func TestBytesToPublicKey_BadBytes(t *testing.T) {
_, err := dsa.BytesToPublicKey(dsa.AlgorithmIDSECP256K1, []byte{0x00, 0x01, 0x02, 0x03})
assert.Error(t, err)
}
func TestBytesToPublicKey_SECP256K1(t *testing.T) {
// vector taken from https://github.com/decentralized-identity/web5-js/blob/dids-new-crypto/packages/crypto/tests/fixtures/test-vectors/secp256k1/bytes-to-public-key.json
publicKeyHex := "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"
pubKeyBytes, err := hex.DecodeString(publicKeyHex)
assert.NoError(t, err)
jwk, err := dsa.BytesToPublicKey(dsa.AlgorithmIDSECP256K1, pubKeyBytes)
assert.NoError(t, err)
assert.Equal(t, ecdsa.SECP256K1JWACurve, jwk.CRV)
assert.Equal(t, ecdsa.KeyType, jwk.KTY)
assert.Equal(t, "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g", jwk.X)
assert.Equal(t, "SDradyajxGVdpPv8DhEIqP0XtEimhVQZnEfQj_sQ1Lg", jwk.Y)
}
func TestPublicKeyToBytes_UnsupportedKTY(t *testing.T) {
_, err := dsa.PublicKeyToBytes(jwk.JWK{KTY: "yolocrypto"})
assert.Error(t, err)
}

View File

@@ -0,0 +1,115 @@
package ecdsa
import (
"errors"
"fmt"
"olares-cli/pkg/web5/jwk"
)
const (
KeyType = "EC"
)
var algorithmIDs = map[string]bool{
SECP256K1AlgorithmID: true,
}
// GeneratePrivateKey generates an ECDSA private key for the given algorithm
func GeneratePrivateKey(algorithmID string) (jwk.JWK, error) {
switch algorithmID {
case SECP256K1AlgorithmID:
return SECP256K1GeneratePrivateKey()
default:
return jwk.JWK{}, fmt.Errorf("unsupported algorithm: %s", algorithmID)
}
}
// GetPublicKey builds an ECDSA public key from the given ECDSA private key
func GetPublicKey(privateKey jwk.JWK) jwk.JWK {
return jwk.JWK{
KTY: privateKey.KTY,
CRV: privateKey.CRV,
X: privateKey.X,
Y: privateKey.Y,
}
}
// Sign generates a cryptographic signature for the given payload with the given private key
//
// # Note
//
// The function will automatically detect the given ECDSA cryptographic curve from the given private key
func Sign(payload []byte, privateKey jwk.JWK) ([]byte, error) {
if privateKey.D == "" {
return nil, errors.New("d must be set")
}
switch privateKey.CRV {
case SECP256K1JWACurve:
return SECP256K1Sign(payload, privateKey)
default:
return nil, fmt.Errorf("unsupported curve: %s", privateKey.CRV)
}
}
// Verify verifies the given signature over a given payload by the given public key
//
// # Note
//
// The function will automatically detect the given ECDSA cryptographic curve from the given public key
func Verify(payload []byte, signature []byte, publicKey jwk.JWK) (bool, error) {
switch publicKey.CRV {
case SECP256K1JWACurve:
return SECP256K1Verify(payload, signature, publicKey)
default:
return false, fmt.Errorf("unsupported curve: %s", publicKey.CRV)
}
}
// GetJWA returns the [JWA] for the given ECDSA key
//
// [JWA]: https://datatracker.ietf.org/doc/html/rfc7518
func GetJWA(jwk jwk.JWK) (string, error) {
switch jwk.CRV {
case SECP256K1JWACurve:
return SECP256K1JWA, nil
default:
return "", fmt.Errorf("unsupported curve: %s", jwk.CRV)
}
}
// BytesToPublicKey deserializes the given byte array into a jwk.JWK for the given cryptographic algorithm
func BytesToPublicKey(algorithmID string, input []byte) (jwk.JWK, error) {
switch algorithmID {
case SECP256K1AlgorithmID:
return SECP256K1BytesToPublicKey(input)
default:
return jwk.JWK{}, fmt.Errorf("unsupported algorithm: %s", algorithmID)
}
}
// PublicKeyToBytes serializes the given public key into a byte array
func PublicKeyToBytes(publicKey jwk.JWK) ([]byte, error) {
switch publicKey.CRV {
case SECP256K1JWACurve:
return SECP256K1PublicKeyToBytes(publicKey)
default:
return nil, fmt.Errorf("unsupported curve: %s", publicKey.CRV)
}
}
// SupportsAlgorithmID informs as to whether or not the given algorithm ID is supported by this package
func SupportsAlgorithmID(id string) bool {
return algorithmIDs[id]
}
// AlgorithmID returns the algorithm ID for the given jwk.JWK
func AlgorithmID(jwk *jwk.JWK) (string, error) {
switch jwk.CRV {
case SECP256K1JWACurve:
return SECP256K1AlgorithmID, nil
default:
return "", fmt.Errorf("unsupported curve: %s", jwk.CRV)
}
}

View File

@@ -0,0 +1,151 @@
package ecdsa
import (
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"olares-cli/pkg/web5/jwk"
_secp256k1 "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
)
const (
SECP256K1JWA string = "ES256K"
SECP256K1JWACurve string = "secp256k1"
SECP256K1AlgorithmID string = SECP256K1JWACurve
)
// SECP256K1GeneratePrivateKey generates a new private key
func SECP256K1GeneratePrivateKey() (jwk.JWK, error) {
keyPair, err := _secp256k1.GeneratePrivateKey()
if err != nil {
return jwk.JWK{}, fmt.Errorf("failed to generate private key: %w", err)
}
dBytes := keyPair.Key.Bytes()
pubKey := keyPair.PubKey()
xBytes := pubKey.X().Bytes()
yBytes := pubKey.Y().Bytes()
privateKey := jwk.JWK{
KTY: KeyType,
CRV: SECP256K1JWACurve,
D: base64.RawURLEncoding.EncodeToString(dBytes[:]),
X: base64.RawURLEncoding.EncodeToString(xBytes),
Y: base64.RawURLEncoding.EncodeToString(yBytes),
}
return privateKey, nil
}
// SECP256K1Sign signs the given payload with the given private key
func SECP256K1Sign(payload []byte, privateKey jwk.JWK) ([]byte, error) {
privateKeyBytes, err := base64.RawURLEncoding.DecodeString(privateKey.D)
if err != nil {
return nil, fmt.Errorf("failed to decode d %w", err)
}
key := _secp256k1.PrivKeyFromBytes(privateKeyBytes)
hash := sha256.Sum256(payload)
signature := ecdsa.SignCompact(key, hash[:], false)[1:]
return signature, nil
}
// SECP256K1Verify verifies the given signature over the given payload with the given public key
func SECP256K1Verify(payload []byte, signature []byte, publicKey jwk.JWK) (bool, error) {
if publicKey.X == "" || publicKey.Y == "" {
return false, errors.New("x and y must be set")
}
hash := sha256.Sum256(payload)
keyBytes, err := secp256k1PublicKeyToUncheckedBytes(publicKey)
if err != nil {
return false, fmt.Errorf("failed to convert public key to bytes: %w", err)
}
key, err := _secp256k1.ParsePubKey(keyBytes)
if err != nil {
return false, fmt.Errorf("failed to parse public key: %w", err)
}
if len(signature) != 64 {
return false, errors.New("signature must be 64 bytes")
}
r := new(_secp256k1.ModNScalar)
r.SetByteSlice(signature[:32])
s := new(_secp256k1.ModNScalar)
s.SetByteSlice(signature[32:])
sig := ecdsa.NewSignature(r, s)
legit := sig.Verify(hash[:], key)
return legit, nil
}
// SECP256K1BytesToPublicKey converts a secp256k1 public key to a JWK.
// Supports both Compressed and Uncompressed public keys described in
// https://www.secg.org/sec1-v2.pdf section 2.3.3
func SECP256K1BytesToPublicKey(input []byte) (jwk.JWK, error) {
pubKey, err := _secp256k1.ParsePubKey(input)
if err != nil {
return jwk.JWK{}, fmt.Errorf("failed to parse public key: %w", err)
}
return jwk.JWK{
KTY: KeyType,
CRV: SECP256K1JWACurve,
X: base64.RawURLEncoding.EncodeToString(pubKey.X().Bytes()),
Y: base64.RawURLEncoding.EncodeToString(pubKey.Y().Bytes()),
}, nil
}
// SECP256K1PublicKeyToBytes converts a secp256k1 public key JWK to bytes.
// Note: this function returns the uncompressed public key. compressed is not
// yet supported
func SECP256K1PublicKeyToBytes(publicKey jwk.JWK) ([]byte, error) {
uncheckedBytes, err := secp256k1PublicKeyToUncheckedBytes(publicKey)
if err != nil {
return nil, err
}
key, err := _secp256k1.ParsePubKey(uncheckedBytes)
if err != nil {
return nil, fmt.Errorf("invalid public key: %w", err)
}
return key.SerializeUncompressed(), nil
}
func secp256k1PublicKeyToUncheckedBytes(publicKey jwk.JWK) ([]byte, error) {
if publicKey.X == "" || publicKey.Y == "" {
return nil, errors.New("x and y must be set")
}
x, err := base64.RawURLEncoding.DecodeString(publicKey.X)
if err != nil {
return nil, fmt.Errorf("failed to decode x: %w", err)
}
y, err := base64.RawURLEncoding.DecodeString(publicKey.Y)
if err != nil {
return nil, fmt.Errorf("failed to decode y: %w", err)
}
// Prepend 0x04 to indicate an uncompressed public key format for secp256k1.
// This byte is a prefix that distinguishes uncompressed keys, which include both X and Y coordinates,
// from compressed keys which only include one coordinate and an indication of the other's parity.
// The secp256k1 standard requires this prefix for uncompressed keys to ensure proper interpretation.
keyBytes := []byte{0x04}
keyBytes = append(keyBytes, x...)
keyBytes = append(keyBytes, y...)
return keyBytes, nil
}

View File

@@ -0,0 +1,99 @@
package ecdsa_test
import (
"encoding/hex"
"testing"
"olares-cli/pkg/web5/crypto/dsa/ecdsa"
"olares-cli/pkg/web5/jwk"
"github.com/alecthomas/assert/v2"
)
func TestSECP256K1GeneratePrivateKey(t *testing.T) {
key, err := ecdsa.SECP256K1GeneratePrivateKey()
assert.NoError(t, err)
assert.Equal(t, ecdsa.KeyType, key.KTY)
assert.Equal(t, ecdsa.SECP256K1JWACurve, key.CRV)
assert.True(t, key.D != "", "privateJwk.D is empty")
assert.True(t, key.X != "", "privateJwk.X is empty")
assert.True(t, key.Y != "", "privateJwk.Y is empty")
}
func TestSECP256K1BytesToPublicKey_Bad(t *testing.T) {
_, err := ecdsa.SECP256K1BytesToPublicKey([]byte{0x00, 0x01, 0x02, 0x03})
assert.Error(t, err)
}
func TestSECP256K1BytesToPublicKey_Uncompressed(t *testing.T) {
// vector taken from https://github.com/decentralized-identity/web5-js/blob/dids-new-crypto/packages/crypto/tests/fixtures/test-vectors/secp256k1/bytes-to-public-key.json
publicKeyHex := "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"
pubKeyBytes, err := hex.DecodeString(publicKeyHex)
assert.NoError(t, err)
jwk, err := ecdsa.SECP256K1BytesToPublicKey(pubKeyBytes)
assert.NoError(t, err)
assert.Equal(t, ecdsa.SECP256K1JWACurve, jwk.CRV)
assert.Equal(t, ecdsa.KeyType, jwk.KTY)
assert.Equal(t, "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g", jwk.X)
assert.Equal(t, "SDradyajxGVdpPv8DhEIqP0XtEimhVQZnEfQj_sQ1Lg", jwk.Y)
}
func TestSECP256K1PublicKeyToBytes(t *testing.T) {
// vector taken from https://github.com/decentralized-identity/web5-js/blob/dids-new-crypto/packages/crypto/tests/fixtures/test-vectors/secp256k1/bytes-to-public-key.json
jwk := jwk.JWK{
KTY: "EC",
CRV: ecdsa.SECP256K1JWACurve,
X: "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g",
Y: "SDradyajxGVdpPv8DhEIqP0XtEimhVQZnEfQj_sQ1Lg",
}
pubKeyBytes, err := ecdsa.SECP256K1PublicKeyToBytes(jwk)
assert.NoError(t, err)
pubKeyHex := hex.EncodeToString(pubKeyBytes)
expected := "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"
assert.Equal(t, expected, pubKeyHex)
}
func TestSECP256K1PublicKeyToBytes_Bad(t *testing.T) {
vectors := []jwk.JWK{
{
KTY: "EC",
CRV: ecdsa.SECP256K1JWACurve,
X: "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g",
},
{
KTY: "EC",
CRV: ecdsa.SECP256K1JWACurve,
Y: "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g",
},
{
KTY: "EC",
CRV: ecdsa.SECP256K1JWACurve,
X: "=///",
Y: "SDradyajxGVdpPv8DhEIqP0XtEimhVQZnEfQj_sQ1Lg",
},
{
KTY: "EC",
CRV: ecdsa.SECP256K1JWACurve,
X: "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g",
Y: "=///",
},
{
KTY: "EC",
CRV: ecdsa.SECP256K1JWACurve,
X: "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g",
Y: "SDradyajxGVdpPv8DhEIqP0XtEimhVQZnEfQj_sQ1Lg2",
},
}
for _, vec := range vectors {
pubKeyBytes, err := ecdsa.SECP256K1PublicKeyToBytes(vec)
assert.Error(t, err)
assert.Equal(t, nil, pubKeyBytes)
}
}

View File

@@ -0,0 +1,82 @@
package eddsa
import (
_ed25519 "crypto/ed25519"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"olares-cli/pkg/web5/jwk"
)
const (
ED25519JWACurve string = "Ed25519"
ED25519AlgorithmID string = ED25519JWACurve
)
// ED25519GeneratePrivateKey generates a new ED25519 private key
func ED25519GeneratePrivateKey() (jwk.JWK, error) {
publicKey, privateKey, err := _ed25519.GenerateKey(rand.Reader)
if err != nil {
return jwk.JWK{}, err
}
privKeyJwk := jwk.JWK{
KTY: KeyType,
CRV: ED25519JWACurve,
D: base64.RawURLEncoding.EncodeToString(privateKey),
X: base64.RawURLEncoding.EncodeToString(publicKey),
}
return privKeyJwk, nil
}
// ED25519Sign signs the given payload with the given private key
func ED25519Sign(payload []byte, privateKey jwk.JWK) ([]byte, error) {
privateKeyBytes, err := base64.RawURLEncoding.DecodeString(privateKey.D)
if err != nil {
return nil, fmt.Errorf("failed to decode d %w", err)
}
signature := _ed25519.Sign(privateKeyBytes, payload)
return signature, nil
}
// ED25519Verify verifies the given signature against the given payload using the given public key
func ED25519Verify(payload []byte, signature []byte, publicKey jwk.JWK) (bool, error) {
publicKeyBytes, err := base64.RawURLEncoding.DecodeString(publicKey.X)
if err != nil {
return false, err
}
legit := _ed25519.Verify(publicKeyBytes, payload, signature)
return legit, nil
}
// ED25519BytesToPublicKey deserializes the byte array into a jwk.JWK public key
func ED25519BytesToPublicKey(input []byte) (jwk.JWK, error) {
if len(input) != _ed25519.PublicKeySize {
return jwk.JWK{}, errors.New("invalid public key")
}
return jwk.JWK{
KTY: KeyType,
CRV: ED25519JWACurve,
X: base64.RawURLEncoding.EncodeToString(input),
}, nil
}
// ED25519PublicKeyToBytes serializes the given public key int a byte array
func ED25519PublicKeyToBytes(publicKey jwk.JWK) ([]byte, error) {
if publicKey.X == "" {
return nil, errors.New("x must be set")
}
publicKeyBytes, err := base64.RawURLEncoding.DecodeString(publicKey.X)
if err != nil {
return nil, fmt.Errorf("failed to decode x %w", err)
}
return publicKeyBytes, nil
}

View File

@@ -0,0 +1,68 @@
package eddsa_test
import (
"encoding/base64"
"encoding/hex"
"testing"
"olares-cli/pkg/web5/crypto/dsa/eddsa"
"olares-cli/pkg/web5/jwk"
"github.com/alecthomas/assert/v2"
)
func TestED25519BytesToPublicKey_Bad(t *testing.T) {
publicKeyBytes := []byte{0x00, 0x01, 0x02, 0x03}
_, err := eddsa.ED25519BytesToPublicKey(publicKeyBytes)
assert.Error(t, err)
}
func TestED25519BytesToPublicKey_Good(t *testing.T) {
// vector taken from https://github.com/decentralized-identity/web5-js/blob/dids-new-crypto/packages/crypto/tests/fixtures/test-vectors/ed25519/bytes-to-public-key.json
pubKeyHex := "7d4d0e7f6153a69b6242b522abbee685fda4420f8834b108c3bdae369ef549fa"
pubKeyBytes, err := hex.DecodeString(pubKeyHex)
assert.NoError(t, err)
jwk, err := eddsa.ED25519BytesToPublicKey(pubKeyBytes)
assert.NoError(t, err)
assert.Equal(t, eddsa.KeyType, jwk.KTY)
assert.Equal(t, eddsa.ED25519JWACurve, jwk.CRV)
assert.Equal(t, "fU0Of2FTpptiQrUiq77mhf2kQg-INLEIw72uNp71Sfo", jwk.X)
}
func TestED25519PublicKeyToBytes(t *testing.T) {
// vector taken from: https://github.com/decentralized-identity/web5-spec/blob/main/test-vectors/crypto_ed25519/sign.json
jwk := jwk.JWK{
KTY: "OKP",
CRV: eddsa.ED25519JWACurve,
X: "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
}
pubKeyBytes, err := eddsa.ED25519PublicKeyToBytes(jwk)
assert.NoError(t, err)
pubKeyB64URL := base64.RawURLEncoding.EncodeToString(pubKeyBytes)
assert.Equal(t, jwk.X, pubKeyB64URL)
}
func TestED25519PublicKeyToBytes_Bad(t *testing.T) {
vectors := []jwk.JWK{
{
KTY: "OKP",
CRV: eddsa.ED25519JWACurve,
},
{
KTY: "OKP",
CRV: eddsa.ED25519JWACurve,
X: "=/---",
},
}
for _, jwk := range vectors {
pubKeyBytes, err := eddsa.ED25519PublicKeyToBytes(jwk)
assert.Error(t, err)
assert.Equal(t, nil, pubKeyBytes)
}
}

View File

@@ -0,0 +1,116 @@
// Package eddsa implements the EdDSA signature schemes as per RFC 8032
// https://tools.ietf.org/html/rfc8032. Note: Currently only Ed25519 is supported
package eddsa
import (
"errors"
"fmt"
"olares-cli/pkg/web5/jwk"
)
const (
JWA string = "EdDSA"
KeyType string = "OKP"
)
var algorithmIDs = map[string]bool{
ED25519AlgorithmID: true,
}
// GeneratePrivateKey generates an EdDSA private key for the given algorithm
func GeneratePrivateKey(algorithmID string) (jwk.JWK, error) {
switch algorithmID {
case ED25519AlgorithmID:
return ED25519GeneratePrivateKey()
default:
return jwk.JWK{}, fmt.Errorf("unsupported algorithm: %s", algorithmID)
}
}
// GetPublicKey builds an EdDSA public key from the given EdDSA private key
func GetPublicKey(privateKey jwk.JWK) jwk.JWK {
return jwk.JWK{
KTY: privateKey.KTY,
CRV: privateKey.CRV,
X: privateKey.X,
}
}
// Sign generates a cryptographic signature for the given payload with the given private key
//
// # Note
//
// The function will automatically detect the given EdDSA cryptographic curve from the given private key
func Sign(payload []byte, privateKey jwk.JWK) ([]byte, error) {
if privateKey.D == "" {
return nil, errors.New("d must be set")
}
switch privateKey.CRV {
case ED25519JWACurve:
return ED25519Sign(payload, privateKey)
default:
return nil, fmt.Errorf("unsupported curve: %s", privateKey.CRV)
}
}
// Verify verifies the given signature over a given payload by the given public key
//
// # Note
//
// The function will automatically detect the given EdDSA cryptographic curve from the given public key
func Verify(payload []byte, signature []byte, publicKey jwk.JWK) (bool, error) {
switch publicKey.CRV {
case ED25519JWACurve:
return ED25519Verify(payload, signature, publicKey)
default:
return false, fmt.Errorf("unsupported curve: %s", publicKey.CRV)
}
}
// GetJWA returns the [JWA] for the given EdDSA key
//
// # Note
//
// The only supported [JWA] is "EdDSA"
//
// [JWA]: https://datatracker.ietf.org/doc/html/rfc7518
func GetJWA(jwk jwk.JWK) (string, error) {
return JWA, nil
}
// BytesToPublicKey deserializes the given byte array into a jwk.JWK for the given cryptographic algorithm
func BytesToPublicKey(algorithmID string, input []byte) (jwk.JWK, error) {
switch algorithmID {
case ED25519AlgorithmID:
return ED25519BytesToPublicKey(input)
default:
return jwk.JWK{}, fmt.Errorf("unsupported algorithm: %s", algorithmID)
}
}
// PublicKeyToBytes serializes the given public key into a byte array
func PublicKeyToBytes(publicKey jwk.JWK) ([]byte, error) {
switch publicKey.CRV {
case ED25519JWACurve:
return ED25519PublicKeyToBytes(publicKey)
default:
return nil, fmt.Errorf("unsupported curve: %s", publicKey.CRV)
}
}
// SupportsAlgorithmID informs as to whether or not the given algorithm ID is supported by this package
func SupportsAlgorithmID(id string) bool {
return algorithmIDs[id]
}
// AlgorithmID returns the algorithm ID for the given jwk.JWK
func AlgorithmID(jwk *jwk.JWK) (string, error) {
switch jwk.CRV {
case ED25519JWACurve:
return ED25519AlgorithmID, nil
default:
return "", fmt.Errorf("unsupported curve: %s", jwk.CRV)
}
}

View File

@@ -0,0 +1,44 @@
package crypto
import (
"crypto/rand"
"encoding/hex"
"errors"
)
// EntropySize represents the size of the entropy in bits, i.e. Entropy128 is equal to 128 bits (or 16 bytes) of entrop
type EntropySize int
// Directly set the sizes according to NIST recommendations for entropy
// defined here: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90Ar1.pdf
const (
Entropy112 EntropySize = 112 / 8 // 14 bytes
Entropy128 EntropySize = 128 / 8 // 16 bytes
Entropy192 EntropySize = 192 / 8 // 24 bytes
Entropy256 EntropySize = 256 / 8 // 32 bytes
)
// GenerateEntropy generates a random byte array of size n bytes
func GenerateEntropy(n EntropySize) ([]byte, error) {
if n <= 0 {
return nil, errors.New("entropy byte size must be > 0")
}
bytes := make([]byte, n)
_, err := rand.Read(bytes)
if err != nil {
return nil, err
}
return bytes, nil
}
// GenerateNonce generates a hex-encoded nonce by calling GenerateEntropy with a size of 16 bytes (128 bits)
func GenerateNonce(n EntropySize) (string, error) {
bytes, err := GenerateEntropy(n)
if err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}

View File

@@ -0,0 +1,58 @@
package crypto_test
import (
"encoding/hex"
"testing"
"olares-cli/pkg/web5/crypto"
"github.com/alecthomas/assert/v2"
)
func Test_GenerateEntropy(t *testing.T) {
bytes, err := crypto.GenerateEntropy(crypto.Entropy128)
assert.NoError(t, err)
assert.Equal(t, int(crypto.Entropy128), len(bytes))
}
func Test_GenerateEntropy_CustomSize(t *testing.T) {
customSize := 99
bytes, err := crypto.GenerateEntropy(crypto.EntropySize(customSize))
assert.NoError(t, err)
assert.Equal(t, customSize, len(bytes))
}
func Test_GenerateEntropy_InvalidSize(t *testing.T) {
bytes, err := crypto.GenerateEntropy(0)
assert.Error(t, err)
assert.Equal(t, nil, bytes)
bytes, err = crypto.GenerateEntropy(-1)
assert.Error(t, err)
assert.Equal(t, nil, bytes)
}
func Test_GenerateNonce(t *testing.T) {
nonce, err := crypto.GenerateNonce(crypto.Entropy128)
assert.NoError(t, err)
assert.Equal(t, int(crypto.Entropy128)*2, len(nonce))
_, err = hex.DecodeString(nonce)
assert.NoError(t, err)
}
func Test_GenerateNonce_CustomSize(t *testing.T) {
customSize := 99
nonce, err := crypto.GenerateNonce(crypto.EntropySize(99))
assert.NoError(t, err)
assert.Equal(t, customSize*2, len(nonce))
_, err = hex.DecodeString(nonce)
assert.NoError(t, err)
}
func Test_GenerateNonce_InvalidSize(t *testing.T) {
nonce, err := crypto.GenerateNonce(0)
assert.Error(t, err)
assert.Equal(t, "", nonce)
}

View File

@@ -0,0 +1,117 @@
package crypto
import (
"fmt"
"olares-cli/pkg/web5/crypto/dsa"
"olares-cli/pkg/web5/jwk"
)
// KeyManager is an abstraction that can be 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
type KeyManager interface {
// GeneratePrivateKey generates a new private key, stores it in the key store and returns the key id
GeneratePrivateKey(algorithmID string) (string, error)
// GetPublicKey returns the public key for the given key id
GetPublicKey(keyID string) (jwk.JWK, error)
// Sign signs the given payload with the private key for the given key id
Sign(keyID string, payload []byte) ([]byte, error)
}
// KeyExporter is an abstraction that can be leveraged to implement types which intend to export keys
type KeyExporter interface {
ExportKey(keyID string) (jwk.JWK, error)
}
// KeyImporter is an abstraction that can be leveraged to implement types which intend to import keys
type KeyImporter interface {
ImportKey(key jwk.JWK) (string, error)
}
// LocalKeyManager is an implementation of KeyManager that stores keys in memory
type LocalKeyManager struct {
keys map[string]jwk.JWK
}
// NewLocalKeyManager returns a new instance of InMemoryKeyManager
func NewLocalKeyManager() *LocalKeyManager {
return &LocalKeyManager{
keys: make(map[string]jwk.JWK),
}
}
// GeneratePrivateKey generates a new private key using the algorithm provided,
// stores it in the key store and returns the key id
// Supported algorithms are available in [olares/olares-cli/pkg/web5/crypto/dsa.AlgorithmID]
func (k *LocalKeyManager) GeneratePrivateKey(algorithmID string) (string, error) {
var keyAlias string
key, err := dsa.GeneratePrivateKey(algorithmID)
if err != nil {
return "", fmt.Errorf("failed to generate private key: %w", err)
}
keyAlias, err = key.ComputeThumbprint()
if err != nil {
return "", fmt.Errorf("failed to compute key alias: %w", err)
}
k.keys[keyAlias] = key
return keyAlias, nil
}
// GetPublicKey returns the public key for the given key id
func (k *LocalKeyManager) GetPublicKey(keyID string) (jwk.JWK, error) {
key, err := k.getPrivateJWK(keyID)
if err != nil {
return jwk.JWK{}, err
}
return dsa.GetPublicKey(key), nil
}
// Sign signs the payload with the private key for the given key id
func (k *LocalKeyManager) Sign(keyID string, payload []byte) ([]byte, error) {
key, err := k.getPrivateJWK(keyID)
if err != nil {
return nil, err
}
return dsa.Sign(payload, key)
}
func (k *LocalKeyManager) getPrivateJWK(keyID string) (jwk.JWK, error) {
key, ok := k.keys[keyID]
if !ok {
return jwk.JWK{}, fmt.Errorf("key with alias %s not found", keyID)
}
return key, nil
}
// ExportKey exports the key specific by the key ID from the [LocalKeyManager]
func (k *LocalKeyManager) ExportKey(keyID string) (jwk.JWK, error) {
key, err := k.getPrivateJWK(keyID)
if err != nil {
return jwk.JWK{}, err
}
return key, nil
}
// ImportKey imports the key into the [LocalKeyManager] and returns the key alias
func (k *LocalKeyManager) ImportKey(key jwk.JWK) (string, error) {
keyAlias, err := key.ComputeThumbprint()
if err != nil {
return "", fmt.Errorf("failed to compute key alias: %w", err)
}
k.keys[keyAlias] = key
return keyAlias, nil
}

View File

@@ -0,0 +1,51 @@
package crypto_test
import (
"testing"
"olares-cli/pkg/web5/crypto"
"olares-cli/pkg/web5/crypto/dsa"
"github.com/alecthomas/assert/v2"
)
func TestGeneratePrivateKey(t *testing.T) {
keyManager := crypto.NewLocalKeyManager()
keyID, err := keyManager.GeneratePrivateKey(dsa.AlgorithmIDSECP256K1)
assert.NoError(t, err)
assert.True(t, keyID != "", "keyID is empty")
}
func TestGetPublicKey(t *testing.T) {
keyManager := crypto.NewLocalKeyManager()
keyID, err := keyManager.GeneratePrivateKey(dsa.AlgorithmIDSECP256K1)
assert.NoError(t, err)
publicKey, err := keyManager.GetPublicKey(keyID)
assert.NoError(t, err)
thumbprint, err := publicKey.ComputeThumbprint()
assert.NoError(t, err)
assert.Equal[string](t, keyID, thumbprint, "unexpected keyID")
}
func TestSign(t *testing.T) {
keyManager := crypto.NewLocalKeyManager()
keyID, err := keyManager.GeneratePrivateKey(dsa.AlgorithmIDSECP256K1)
assert.NoError(t, err)
payload := []byte("hello world")
signature, err := keyManager.Sign(keyID, payload)
assert.NoError(t, err)
if signature == nil {
t.Errorf("signature is nil")
}
assert.True(t, signature != nil, "signature is nil")
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

283
cli/pkg/web5/dids/README.md Normal file
View File

@@ -0,0 +1,283 @@
# dids <!-- omit in toc -->
# Table of Contents <!-- omit in toc -->
- [Features](#features)
- [Usage](#usage)
- [DID Creation](#did-creation)
- [`did:jwk`](#didjwk)
- [`did:dht`](#diddht)
- [`did:web`](#didweb)
- [DID Resolution](#did-resolution)
- [Importing / Exporting](#importing--exporting)
- [Exporting](#exporting)
- [Importing](#importing)
- [Development](#development)
- [Directory Structure](#directory-structure)
- [Rationale](#rationale)
- [Adding a new DID Method](#adding-a-new-did-method)
- [Creation](#creation)
- [Resolution](#resolution)
# Features
* `did:jwk` creation and resolution
* `did:dht` creation and resoluton
* DID Parsing
* `BearerDID` concept.
* `BearerDID` import and export
* All did core spec data structures
* singleton DID resolver
> [!NOTE]
> This package uses the term `DID` to refer to the string representation e.g. `did:ex:1234` and `BearerDID` to refer to is a composite type that combines a DID with a KeyManager containing keys associated to the DID. Together, these two components form a BearerDID that can be used to
sign data.
> [!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`
```go
package main
import (
"fmt"
"github.com/beclab/Olares/cli/pkg/web5/didjwk"
)
func main() {
// Create a new DID
bearerDID, err := didjwk.Create()
if err != nil {
fmt.Printf("Failed to create new DID: %v\n", err)
return
}
fmt.Printf("New DID created: %s\n", bearerDID.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:
```go
package main
import (
"fmt"
"github.com/beclab/Olares/cli/pkg/web5/didjwk"
)
func main() {
km, err := AWSKeyManager()
if err != nil {
fmt.Errorf("failed to initialize AWS Key Manager. %w", err)
}
bearerDID, err := didjwk.Create(KeyManager(km))
if err != nil {
fmt.Printf("Failed to create new DID: %v\n", err)
return
}
fmt.Printf("New DID created: %s\n", bearerDID.URI)
}
```
> [!WARNING]
> `AWSKeyManager` doesn't exist yet in `web5-go` but will soon
Overriding the default Alogithm ID can be done like so:
```go
package main
import (
"fmt"
"github.com/beclab/Olares/cli/pkg/web5/dsa"
"github.com/beclab/Olares/cli/pkg/web5/didjwk"
)
func main() {
bearerDID, err := didjwk.Create(AlgorithmID(dsa.AlgorithmIDED25519))
if err != nil {
fmt.Printf("Failed to create new DID: %v\n", err)
return
}
fmt.Printf("New DID created: %s\n", bearerDID.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
### `did:dht`
> [!WARNING]
> TODO: Fill out
### `did:web`
> [!WARNING]
> TODO: Fill out
## DID Resolution
this package provides a preconfigured resolver that is capable of resolving all of the did methods included in this module
```go
package main
import (
"fmt"
"github.com/beclab/Olares/cli/pkg/web5/dsa"
"github.com/beclab/Olares/cli/pkg/web5/dids"
)
func main() {
resolutionResult, err := dids.Resolve("did:ex:123")
if err != nil {
fmt.Printf("Failed to resolve DID: %v\n", err)
return
}
}
```
## Importing / Exporting
In scenarios where a Secrets Manager is being used instead of a HSM based KMS, you'll want to:
* create your DID (aka `BearerDID`) _once_,
* export it as a `PortableDID`
* save `PortableDID` in a Secrets Manager,
* import the `PortableDID` into a `BearerDID` in order to use it to sign things etc.
> [!IMPORTANT]
> this SDK will contain a `cmd` to create a DID and output a Portable DID soon!
### Exporting
Exporting can be done like so:
```go
package main
import (
"fmt"
"github.com/beclab/Olares/cli/pkg/web5/did"
"github.com/beclab/Olares/cli/pkg/web5/didjwk"
)
func main() {
bearerDID, err := didjwk.Create()
if err != nil {
fmt.Printf("Failed to create new DID: %v\n", err)
return
}
portableDID, _ := bearerDID.ToKeys()
bytes, _ := json.Marshal(&portableDID)
fmt.Println(string(bytes)) // SAVE OUTPUT somewhere safe
}
```
> [!WARNING]
> `bearerDID.ToKeys()` will be renamed to `bearerDID.ToPortableDID()`
### Importing
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
```go
package main
import (
"fmt"
"github.com/beclab/Olares/cli/pkg/web5/did"
)
func main() {
portableDID := os.Getenv("SEC_DID")
bearerDID, err := did.BearerDIDFromKeys(portableDID)
}
```
> [!WARNING]
> `did.BearerDIDFromKeys(portableDID)` will be renamed `did.FromPortableDID`
# Development
## Directory Structure
```
dids
├── README.md
├── did
│   ├── bearerdid.go
│   ├── bearerdid_test.go
│   ├── did.go
│   └── did_test.go
├── didcore
│   ├── document.go
│   ├── document_test.go
│   └── resolution.go
├── diddht
│   ├── diddht.go
│   └── diddht_test.go
├── didjwk
│   ├── didjwk.go
│   └── didjwk_test.go
└── resolver.go
```
| package | description |
| :------------ | :---------------------------------------------------------------------------------------------- |
| `did` | contains _representations_ of a DID. |
| `didcore` | contains all of the data models defined in the [DID Core Spec](https://www.w3.org/TR/did-core/) |
| `did<method>` | one package for each did method |
| `dids` | high-level APIs that support multiple DID methods |
### Rationale
The primary goals for the api surface for dids is to:
* self-contain each did method in its own package in order to provide an api surface that looks like `didjwk.Create()`, `diddht.Create` etc.
* provide a `Resolve` method capable of resolving all did methods in this module without any configuration or registration
The directory/package structure is a result of achieving both goals in a way that hopefully makes logical sense and prevents cyclic imports.
Internal Dependency Diagram:
![](../diagrams/dids-pkg.png)
## Adding a new DID Method
* Create a package for the did method being implemented e.g. `didjwk`, `diddht`, `didweb`
### Creation
* Other did methods in this module include a `Create` method that creates a _new_ `BearerDID`
* Preferrably `Create` should work without having to pass it any arguments
* Options should be provided using the functional options pattern described [here](https://golang.cafe/blog/golang-functional-options-pattern.html) (thanks for the suggestion @alecthomas)
### Resolution
* Implement the [MethodResolver] interface defined in the `didcore` package
* plug the method resolver into `dids/resolver.go`

View File

@@ -0,0 +1,113 @@
package did
import (
"fmt"
"olares-cli/pkg/web5/crypto"
"olares-cli/pkg/web5/dids/didcore"
"olares-cli/pkg/web5/jwk"
)
// BearerDID is a composite type that combines a DID with a KeyManager containing keys
// associated to the DID. Together, these two components form a BearerDID that can be used to
// sign and verify data.
type BearerDID struct {
DID
crypto.KeyManager
Document didcore.Document
}
// DIDSigner is a function returned by GetSigner that can be used to sign a payload with a key
// associated to a BearerDID.
type DIDSigner func(payload []byte) ([]byte, error)
// ToPortableDID exports a BearerDID to a portable format
func (d *BearerDID) ToPortableDID() (PortableDID, error) {
portableDID := PortableDID{
URI: d.URI,
Document: d.Document,
}
exporter, ok := d.KeyManager.(crypto.KeyExporter)
if ok {
privateKeys := make([]jwk.JWK, 0)
for _, vm := range d.Document.VerificationMethod {
keyAlias, err := vm.PublicKeyJwk.ComputeThumbprint()
if err != nil {
continue
}
key, err := exporter.ExportKey(keyAlias)
if err != nil {
// TODO: decide if we want to blow up or continue
continue
}
privateKeys = append(privateKeys, key)
}
portableDID.PrivateKeys = privateKeys
}
return portableDID, nil
}
// GetSigner returns a sign method that can be used to sign a payload using a key associated to the DID.
// This function also returns the verification method needed to verify the signature.
//
// Providing the verification method allows the caller to provide the signature's recipient
// with a reference to the verification method needed to verify the payload. This is often done
// by including the verification method id either alongside the signature or as part of the header
// in the case of JSON Web Signatures.
//
// The verifier can dereference the verification method id to obtain the public key needed to verify the signature.
//
// This function takes a Verification Method selector that can be used to select a specific verification method
// from the DID Document if desired. If no selector is provided, the payload will be signed with the key associated
// to the first verification method in the DID Document.
//
// The selector can either be a Verification Method ID or a Purpose. If a Purpose is provided, the first verification
// method in the DID Document that has the provided purpose will be used to sign the payload.
//
// The returned signer is a function that takes a byte payload and returns a byte signature.
func (d *BearerDID) GetSigner(selector didcore.VMSelector) (DIDSigner, didcore.VerificationMethod, error) {
vm, err := d.Document.SelectVerificationMethod(selector)
if err != nil {
return nil, didcore.VerificationMethod{}, err
}
keyAlias, err := vm.PublicKeyJwk.ComputeThumbprint()
if err != nil {
return nil, didcore.VerificationMethod{}, fmt.Errorf("failed to compute key alias: %s", err.Error())
}
signer := func(payload []byte) ([]byte, error) {
return d.Sign(keyAlias, payload)
}
return signer, vm, nil
}
// FromPortableDID inflates a BearerDID from a portable format.
func FromPortableDID(portableDID PortableDID) (BearerDID, error) {
did, err := Parse(portableDID.URI)
if err != nil {
return BearerDID{}, err
}
keyManager := crypto.NewLocalKeyManager()
for _, key := range portableDID.PrivateKeys {
_, err := keyManager.ImportKey(key)
if err != nil {
// todo what should we do here?
return BearerDID{}, err
}
}
return BearerDID{
DID: did,
KeyManager: keyManager,
Document: portableDID.Document,
}, nil
}

View File

@@ -0,0 +1,69 @@
package did_test
import (
"testing"
"olares-cli/pkg/web5/crypto/dsa"
"olares-cli/pkg/web5/dids/did"
"olares-cli/pkg/web5/dids/didcore"
"olares-cli/pkg/web5/dids/didkey"
"olares-cli/pkg/web5/jwk"
"olares-cli/pkg/web5/jws"
"github.com/alecthomas/assert/v2"
)
func TestToPortableDID(t *testing.T) {
did, err := didkey.Create()
assert.NoError(t, err)
portableDID, err := did.ToPortableDID()
assert.NoError(t, err)
assert.Equal[string](t, did.URI, portableDID.URI)
assert.True(t, len(portableDID.PrivateKeys) == 1, "expected 1 key")
key := portableDID.PrivateKeys[0]
assert.NotEqual(t, jwk.JWK{}, key, "expected key to not be empty")
}
func TestFromPortableDID(t *testing.T) {
bearerDID, err := didkey.Create()
assert.NoError(t, err)
portableDID, err := bearerDID.ToPortableDID()
assert.NoError(t, err)
importedDID, err := did.FromPortableDID(portableDID)
assert.NoError(t, err)
payload := []byte("hi")
compactJWS, err := jws.Sign(payload, bearerDID)
assert.NoError(t, err)
compactJWSAgane, err := jws.Sign(payload, importedDID)
assert.NoError(t, err)
assert.Equal[string](t, compactJWS, compactJWSAgane, "failed to produce same signature with imported did")
}
func TestGetSigner(t *testing.T) {
bearerDID, err := didkey.Create()
assert.NoError(t, err)
sign, vm, err := bearerDID.GetSigner(nil)
assert.NoError(t, err)
assert.NotEqual(t, vm, didcore.VerificationMethod{}, "expected verification method to not be empty")
payload := []byte("hi")
signature, err := sign(payload)
assert.NoError(t, err)
legit, err := dsa.Verify(payload, signature, *vm.PublicKeyJwk)
assert.NoError(t, err)
assert.True(t, legit, "expected signature to be valid")
}

View File

@@ -0,0 +1,170 @@
package did
import (
"database/sql/driver"
"errors"
"fmt"
"regexp"
"strings"
)
// DID provides a way to parse and handle Decentralized Identifier (DID) URIs
// according to the W3C DID Core specification (https://www.w3.org/TR/did-core/).
type DID struct {
// URI represents the complete Decentralized Identifier (DID) URI.
// Spec: https://www.w3.org/TR/did-core/#did-syntax
URI string
// Method specifies the DID method in the URI, which indicates the underlying
// method-specific identifier scheme (e.g., jwk, dht, key, etc.).
// Spec: https://www.w3.org/TR/did-core/#method-schemes
Method string
// ID is the method-specific identifier in the DID URI.
// Spec: https://www.w3.org/TR/did-core/#method-specific-id
ID string
// Params is a map containing optional parameters present in the DID URI.
// These parameters are method-specific.
// Spec: https://www.w3.org/TR/did-core/#did-parameters
Params map[string]string
// Path is an optional path component in the DID URI.
// Spec: https://www.w3.org/TR/did-core/#path
Path string
// Query is an optional query component in the DID URI, used to express a request
// for a specific representation or resource related to the DID.
// Spec: https://www.w3.org/TR/did-core/#query
Query string
// Fragment is an optional fragment component in the DID URI, used to reference
// a specific part of a DID document.
// Spec: https://www.w3.org/TR/did-core/#fragment
Fragment string
}
// URL represents the DID URI + A network location identifier for a specific resource
// Spec: https://www.w3.org/TR/did-core/#did-url-syntax
func (d DID) URL() string {
url := d.URI
if len(d.Params) > 0 {
var pairs []string
for key, value := range d.Params {
pairs = append(pairs, fmt.Sprintf("%s=%s", key, value))
}
url += ";" + strings.Join(pairs, ";")
}
if len(d.Path) > 0 {
url += "/" + d.Path
}
if len(d.Query) > 0 {
url += "?" + d.Query
}
if len(d.Fragment) > 0 {
url += "#" + d.Fragment
}
return url
}
func (d DID) String() string {
return d.URL()
}
// MarshalText will convert the given DID's URL into a byte array
func (d DID) MarshalText() (text []byte, err error) {
return []byte(d.String()), nil
}
// UnmarshalText will deserialize the given byte array into an instance of [DID]
func (d *DID) UnmarshalText(text []byte) error {
did, err := Parse(string(text))
if err != nil {
return err
}
*d = did
return nil
}
// Scan implements the Scanner interface
func (d *DID) Scan(src any) error {
switch obj := src.(type) {
case nil:
return nil
case string:
if src == "" {
return nil
}
return d.UnmarshalText([]byte(obj))
default:
return fmt.Errorf("unsupported scan type %T", obj)
}
}
// Value implements the driver Valuer interface
func (d DID) Value() (driver.Value, error) {
return d.String(), nil
}
// relevant ABNF rules: https://www.w3.org/TR/did-core/#did-syntax
var (
pctEncodedPattern = `(?:%[0-9a-fA-F]{2})`
idCharPattern = `(?:[a-zA-Z0-9._-]|` + pctEncodedPattern + `)`
methodPattern = `([a-z0-9]+)`
methodIDPattern = `((?:` + idCharPattern + `*:)*(` + idCharPattern + `+))`
paramCharPattern = `[a-zA-Z0-9_.:%-]`
paramPattern = `;` + paramCharPattern + `+=` + paramCharPattern + `*`
paramsPattern = `((` + paramPattern + `)*)`
pathPattern = `(/[^#?]*)?`
queryPattern = `(\?[^\#]*)?`
fragmentPattern = `(\#.*)?`
didURIPattern = regexp.MustCompile(`^did:` + methodPattern + `:` + methodIDPattern + paramsPattern + pathPattern + queryPattern + fragmentPattern + `$`)
)
// Parse parses a DID URI in accordance to the ABNF rules specified in the
// specification here: https://www.w3.org/TR/did-core/#did-syntax. Returns
// a DIDURI instance if parsing is successful. Otherwise, returns an error.
func Parse(input string) (DID, error) {
match := didURIPattern.FindStringSubmatch(input)
if match == nil {
return DID{}, errors.New("invalid DID URI")
}
did := DID{
URI: "did:" + match[1] + ":" + match[2],
Method: match[1],
ID: match[2],
}
if len(match[4]) > 0 {
params := strings.Split(match[4][1:], ";")
parsedParams := make(map[string]string)
for _, p := range params {
kv := strings.Split(p, "=")
parsedParams[kv[0]] = kv[1]
}
did.Params = parsedParams
}
if match[6] != "" {
did.Path = match[6]
}
if match[7] != "" {
did.Query = match[7][1:]
}
if match[8] != "" {
did.Fragment = match[8][1:]
}
return did, nil
}
// MustParse parses a DID URI with Parse, and panics on error
func MustParse(input string) DID {
did, err := Parse(input)
if err != nil {
panic(err)
}
return did
}

View File

@@ -0,0 +1,193 @@
package did_test
import (
"testing"
"olares-cli/pkg/web5/dids/did"
"github.com/alecthomas/assert/v2"
)
type vector struct {
input string
output map[string]interface{}
error bool
}
func TestParse(t *testing.T) {
vectors := []vector{
{input: "", error: true},
{input: "did:", error: true},
{input: "did:uport", error: true},
{input: "did:uport:", error: true},
{input: "did:uport:1234_12313***", error: true},
{input: "2nQtiQG6Cgm1GYTBaaKAgr76uY7iSexUkqX", error: true},
{input: "did:method:%12%1", error: true},
{input: "did:method:%1233%Ay", error: true},
{input: "did:CAP:id", error: true},
{input: "did:method:id::anotherid%r9", error: true},
{
input: "did:example:123456789abcdefghi",
output: map[string]interface{}{
"method": "example",
"id": "123456789abcdefghi",
"uri": "did:example:123456789abcdefghi",
},
},
{
input: "did:example:123456789abcdefghi;foo=bar;baz=qux",
output: map[string]interface{}{
"alternate": "did:example:123456789abcdefghi;baz=qux;foo=bar",
"method": "example",
"id": "123456789abcdefghi",
"uri": "did:example:123456789abcdefghi",
"params": map[string]string{
"foo": "bar",
"baz": "qux",
},
},
},
{
input: "did:example:123456789abcdefghi?foo=bar&baz=qux",
output: map[string]interface{}{
"method": "example",
"id": "123456789abcdefghi",
"uri": "did:example:123456789abcdefghi",
"query": "foo=bar&baz=qux",
},
},
{
input: "did:example:123456789abcdefghi#keys-1",
output: map[string]interface{}{
"method": "example",
"id": "123456789abcdefghi",
"uri": "did:example:123456789abcdefghi",
"fragment": "keys-1",
},
},
{
input: "did:example:123456789abcdefghi?foo=bar&baz=qux#keys-1",
output: map[string]interface{}{
"method": "example",
"id": "123456789abcdefghi",
"uri": "did:example:123456789abcdefghi",
"query": "foo=bar&baz=qux",
"fragment": "keys-1",
},
},
{
input: "did:example:123456789abcdefghi;foo=bar;baz=qux?p1=v1&p2=v2#keys-1",
output: map[string]interface{}{
"alternate": "did:example:123456789abcdefghi;baz=quxfoo=bar;?p1=v1&p2=v2#keys-1",
"method": "example",
"id": "123456789abcdefghi",
"uri": "did:example:123456789abcdefghi",
"params": map[string]string{"foo": "bar", "baz": "qux"},
"query": "p1=v1&p2=v2",
"fragment": "keys-1",
},
},
}
for _, v := range vectors {
t.Run(v.input, func(t *testing.T) {
did, err := did.Parse(v.input)
if v.error && err == nil {
t.Errorf("expected error, got nil")
}
if err != nil {
if !v.error {
t.Errorf("failed to parse did: %s", err.Error())
}
return
}
// The Params map doesn't have a reliable order, so check both
alt, ok := v.output["alternate"]
if ok {
firstOrder := v.input == did.URL()
secondOrder := alt == did.URL()
assert.True(t, firstOrder || secondOrder, "expected one of the orders to match")
} else {
assert.Equal[interface{}](t, v.input, did.URL())
}
assert.Equal[interface{}](t, v.output["method"], did.Method)
assert.Equal[interface{}](t, v.output["id"], did.ID)
assert.Equal[interface{}](t, v.output["uri"], did.URI)
if v.output["params"] != nil {
params, ok := v.output["params"].(map[string]string)
assert.True(t, ok, "expected params to be map[string]string")
for k, v := range params {
assert.Equal[interface{}](t, v, did.Params[k])
}
}
if v.output["query"] != nil {
assert.Equal[interface{}](t, v.output["query"], did.Query)
}
if v.output["fragment"] != nil {
assert.Equal[interface{}](t, v.output["fragment"], did.Fragment)
}
})
}
}
func TestDID_ScanValueRoundtrip(t *testing.T) {
tests := []struct {
object did.DID
raw string
alt string
wantErr bool
}{
{
raw: "did:example:123456789abcdefghi",
object: did.MustParse("did:example:123456789abcdefghi"),
},
{
raw: "did:example:123456789abcdefghi;foo=bar;baz=qux",
alt: "did:example:123456789abcdefghi;baz=qux;foo=bar",
object: did.MustParse("did:example:123456789abcdefghi;foo=bar;baz=qux"),
},
{
raw: "did:example:123456789abcdefghi?foo=bar&baz=qux",
object: did.MustParse("did:example:123456789abcdefghi?foo=bar&baz=qux"),
},
{
raw: "did:example:123456789abcdefghi#keys-1",
object: did.MustParse("did:example:123456789abcdefghi#keys-1"),
},
{
raw: "did:example:123456789abcdefghi?foo=bar&baz=qux#keys-1",
object: did.MustParse("did:example:123456789abcdefghi?foo=bar&baz=qux#keys-1"),
},
{
raw: "did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1",
alt: "did:example:123456789abcdefghi;baz=qux;foo=bar?foo=bar&baz=qux#keys-1",
object: did.MustParse("did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1"),
},
}
for _, tt := range tests {
t.Run(tt.raw, func(t *testing.T) {
var d did.DID
if err := d.Scan(tt.raw); (err != nil) != tt.wantErr {
t.Errorf("Scan() error = %v, wantErr %v", err, tt.wantErr)
}
assert.Equal(t, tt.object, d)
value, err := d.Value()
assert.NoError(t, err)
actual, ok := value.(string)
assert.True(t, ok)
if tt.alt != "" {
assert.True(t, actual == tt.raw || actual == tt.alt)
} else {
assert.Equal(t, tt.raw, actual)
}
})
}
}

View File

@@ -0,0 +1,22 @@
package did
import (
"olares-cli/pkg/web5/dids/didcore"
"olares-cli/pkg/web5/jwk"
)
// PortableDID is a serializable BearerDID. VerificationMethod contains the private key
// of each verification method that the BearerDID's key manager contains
type PortableDID struct {
// URI is the DID string as per https://www.w3.org/TR/did-core/#did-syntax
URI string `json:"uri"`
// PrivateKeys is an array of private keys associated to the BearerDID's verification methods
// Note: PrivateKeys will be empty if the BearerDID was created using a KeyManager that does not
// support exporting private keys (e.g. HSM based KeyManagers)
PrivateKeys []jwk.JWK `json:"privateKeys"`
// Document is the DID Document associated to the BearerDID
Document didcore.Document `json:"document"`
// Metadata is a map that can be used to store additional method specific data
// that is necessary to inflate a BearerDID from a PortableDID
Metadata map[string]interface{} `json:"metadata"`
}

View File

@@ -0,0 +1,297 @@
package didcore
import (
"errors"
"fmt"
"olares-cli/pkg/web5/jwk"
)
const (
PurposeAssertion Purpose = "assertionMethod"
PurposeAuthentication Purpose = "authentication"
PurposeCapabilityDelegation Purpose = "capabilityDelegation"
PurposeCapabilityInvocation Purpose = "capabilityInvocation"
PurposeKeyAgreement Purpose = "keyAgreement"
)
// Document represents a set of data describing the DID subject including mechanisms such as:
// - cryptographic public keys - used to authenticate itself and prove
// association with the DID
// - services - means of communicating or interacting with the DID subject or
// associated entities via one or more service endpoints.
// Examples include discovery services, agent services,
// social networking services, file storage services,
// and verifiable credential repository services.
//
// A DID Document can be retrieved by resolving a DID URI.
type Document struct {
// Context is a URI that defines the schema version used in the document.
Context []string `json:"@context,omitempty"`
// Id is the DID URI for a particular DID subject, expressed using the id property in the DID document.
ID string `json:"id"`
// AlsoKnownAs can contain multiple identifiers for different purposes, or at different times for the same DID subject.
// The assertion that two or more DIDs (or other types of URI) refer to the same DID subject can be made using the alsoKnownAs property.
AlsoKnownAs []string `json:"alsoKnownAs,omitempty"`
// Controller defines an entity that is authorized to make changes to a DID document.
// The process of authorizing a DID controller is defined by the DID method.
// It can be a string or a list of strings.
Controller []string `json:"controller,omitempty"`
// VerificationMethod is a list of cryptographic public keys, which can be used to authenticate or authorize
// interactions with the DID subject or associated parties.
VerificationMethod []VerificationMethod `json:"verificationMethod,omitempty"`
// Service expresses ways of communicating with the DID subject or associated entities.
// A service can be any type of service the DID subject wants to advertise.
// spec reference: https://www.w3.org/TR/did-core/#verification-methods
Service []Service `json:"service,omitempty"`
// AssertionMethod is used to specify how the DID subject is expected to express claims,
// such as for the purposes of issuing a Verifiable Credential.
AssertionMethod []string `json:"assertionMethod,omitempty"`
// Authentication specifies how the DID subject is expected to be authenticated,
// for purposes such as logging into a website or engaging in any sort of challenge-response protocol.
Authentication []string `json:"authentication,omitempty"`
// KeyAgreement specifies how an entity can generate encryption material to transmit confidential
// information intended for the DID subject, such as for establishing a secure communication channel.
KeyAgreement []string `json:"keyAgreement,omitempty"`
// CapabilityDelegation specifies a mechanism used by the DID subject to delegate a
// cryptographic capability to another party, such as delegating the authority to access a specific HTTP API.
CapabilityDelegation []string `json:"capabilityDelegation,omitempty"`
// CapabilityInvocation specifies a verification method used by the DID subject to invoke a
// cryptographic capability, such as the authorization to update the DID Document.
CapabilityInvocation []string `json:"capabilityInvocation,omitempty"`
}
type addVMOptions struct {
purposes []Purpose
}
// AddVMOption is a type returned by all AddVerificationMethod options for variadic parameter support
type AddVMOption func(o *addVMOptions)
// Purposes can be used to select a verification method with a specific purpose.
func Purposes(p ...Purpose) AddVMOption {
return func(o *addVMOptions) {
o.purposes = p
}
}
// AddVerificationMethod adds a verification method to the document. if Purposes are provided,
// the verification method's ID will be added to the corresponding list of purposes.
func (d *Document) AddVerificationMethod(method VerificationMethod, opts ...AddVMOption) {
o := &addVMOptions{purposes: []Purpose{}}
for _, opt := range opts {
opt(o)
}
d.VerificationMethod = append(d.VerificationMethod, method)
for _, p := range o.purposes {
switch p {
case PurposeAssertion:
d.AssertionMethod = append(d.AssertionMethod, method.ID)
case PurposeAuthentication:
d.Authentication = append(d.Authentication, method.ID)
case PurposeKeyAgreement:
d.KeyAgreement = append(d.KeyAgreement, method.ID)
case PurposeCapabilityDelegation:
d.CapabilityDelegation = append(d.CapabilityDelegation, method.ID)
case PurposeCapabilityInvocation:
d.CapabilityInvocation = append(d.CapabilityInvocation, method.ID)
}
}
}
// VMSelector is an interface that can be implemented to provide a means to select
// a specific verification method from a DID Document.
type VMSelector interface {
selector()
}
// Purpose can be used to select a verification method with a specific purpose.
type Purpose string
func (p Purpose) selector() {}
// ID can be used to select a verification method by its ID.
type ID string
func (i ID) selector() {}
// SelectVerificationMethod takes a selector that can be used to select a specific verification
// method from the DID Document. If a nil selector is provided, the first verification method
// is returned
//
// The selector can either be an ID, Purpose, or nil. If a Purpose is provided, the first verification
// method in the DID Document that has the provided purpose will be returned.
func (d *Document) SelectVerificationMethod(selector VMSelector) (VerificationMethod, error) {
if len(d.VerificationMethod) == 0 {
return VerificationMethod{}, errors.New("no verification methods found")
}
if selector == nil {
return d.VerificationMethod[0], nil
}
var vmID string
switch s := selector.(type) {
case Purpose:
switch s {
case PurposeAssertion:
if len(d.AssertionMethod) == 0 {
return VerificationMethod{}, fmt.Errorf("no verification method found for purpose: %s", s)
}
vmID = d.AssertionMethod[0]
case PurposeAuthentication:
if len(d.Authentication) == 0 {
return VerificationMethod{}, fmt.Errorf("no %s verification method found", s)
}
vmID = d.Authentication[0]
case PurposeCapabilityDelegation:
if len(d.CapabilityDelegation) == 0 {
return VerificationMethod{}, fmt.Errorf("no %s verification method found", s)
}
vmID = d.CapabilityDelegation[0]
case PurposeCapabilityInvocation:
if len(d.CapabilityInvocation) == 0 {
return VerificationMethod{}, fmt.Errorf("no %s verification method found", s)
}
vmID = d.CapabilityInvocation[0]
case PurposeKeyAgreement:
if len(d.KeyAgreement) == 0 {
return VerificationMethod{}, fmt.Errorf("no %s verification method found", s)
}
vmID = d.KeyAgreement[0]
default:
return VerificationMethod{}, fmt.Errorf("unsupported purpose: %s", s)
}
case ID:
vmID = string(s)
}
for _, vm := range d.VerificationMethod {
if vm.ID == vmID {
return vm, nil
}
}
return VerificationMethod{}, fmt.Errorf("no verification method found for id: %s", vmID)
}
// AddService will append the given Service to the Document.Services array
func (d *Document) AddService(service Service) {
d.Service = append(d.Service, service)
}
// GetAbsoluteResourceID returns a fully qualified ID for a document resource (e.g. service, verification method)
// Document Resource IDs are allowed to be relative DID URLs as a means to reduce storage size of DID Documents.
// More info here: https://www.w3.org/TR/did-core/#relative-did-urls
func (d *Document) GetAbsoluteResourceID(id string) string {
if id[0] == '#' {
return d.ID + id
}
return id
}
// DocumentMetadata contains metadata about the DID Document
// This metadata typically does not change between invocations of
// the resolve and resolveRepresentation functions unless the DID document
// changes, as it represents metadata about the DID document.
//
// Spec: https://www.w3.org/TR/did-core/#dfn-diddocumentmetadata
type DocumentMetadata struct {
// timestamp of the Create operation. The value of the property MUST be a
// string formatted as an XML Datetime normalized to UTC 00:00:00 and
// without sub-second decimal precision. For example: 2020-12-20T19:17:47Z.
Created string `json:"created,omitempty"`
// timestamp of the last Update operation for the document version which was
// resolved. The value of the property MUST follow the same formatting rules
// as the created property. The updated property is omitted if an Update
// operation has never been performed on the DID document. If an updated
// property exists, it can be the same value as the created property
// when the difference between the two timestamps is less than one second.
Updated string `json:"updated,omitempty"`
// If a DID has been deactivated, DID document metadata MUST include this
// property with the boolean value true. If a DID has not been deactivated,
// this property is OPTIONAL, but if included, MUST have the boolean value
// false.
Deactivated bool `json:"deactivated,omitempty"`
// indicates the version of the last Update operation for the document version
// which was resolved.
VersionID string `json:"versionId,omitempty"`
// indicates the timestamp of the next Update operation. The value of the
// property MUST follow the same formatting rules as the created property.
NextUpdate string `json:"nextUpdate,omitempty"`
// if the resolved document version is not the latest version of the document.
// It indicates the timestamp of the next Update operation. The value of the
// property MUST follow the same formatting rules as the created property.
NextVersionID string `json:"nextVersionId,omitempty"`
// A DID method can define different forms of a DID that are logically
// equivalent. An example is when a DID takes one form prior to registration
// in a verifiable data registry and another form after such registration.
// In this case, the DID method specification might need to express one or
// more DIDs that are logically equivalent to the resolved DID as a property
// of the DID document. This is the purpose of the equivalentId property.
EquivalentID []string `json:"equivalentId,omitempty"`
// The canonicalId property is identical to the equivalentId property except:
// * it is associated with a single value rather than a set
// * the DID is defined to be the canonical ID for the DID subject within
// the scope of the containing DID document.
CanonicalID string `json:"canonicalId,omitempty"`
}
// Service is used in DID documents to express ways of communicating with
// the DID subject or associated entities.
// A service can be any type of service the DID subject wants to advertise.
//
// Specification Reference: https://www.w3.org/TR/did-core/#services
type Service struct {
// Id is the value of the id property and MUST be a URI conforming to RFC3986.
// A conforming producer MUST NOT produce multiple service entries with
// the same id. A conforming consumer MUST produce an error if it detects
// multiple service entries with the same id.
ID string `json:"id"`
// Type is an example of registered types which can be found
// here: https://www.w3.org/TR/did-spec-registries/#service-types
Type string `json:"type"`
// ServiceEndpoint is a network address, such as an HTTP URL, at which services
// operate on behalf of a DID subject.
ServiceEndpoint []string `json:"serviceEndpoint"`
}
// VerificationMethod expresses verification methods, such as cryptographic
// public keys, which can be used to authenticate or authorize interactions
// with the DID subject or associated parties. For example,
// a cryptographic public key can be used as a verification method with
// respect to a digital signature; in such usage, it verifies that the
// signer could use the associated cryptographic private key.
//
// Specification Reference: https://www.w3.org/TR/did-core/#verification-methods
type VerificationMethod struct {
ID string `json:"id"`
// references exactly one verification method type. In order to maximize global
// interoperability, the verification method type SHOULD be registered in the
// DID Specification Registries: https://www.w3.org/TR/did-spec-registries/
Type string `json:"type"`
// a value that conforms to the rules in DID Syntax: https://www.w3.org/TR/did-core/#did-syntax
Controller string `json:"controller"`
// specification reference: https://www.w3.org/TR/did-core/#dfn-publickeyjwk
PublicKeyJwk *jwk.JWK `json:"publicKeyJwk,omitempty"`
}

View File

@@ -0,0 +1,44 @@
package didcore_test
import (
"testing"
"olares-cli/pkg/web5/dids/didcore"
"github.com/alecthomas/assert/v2"
)
func TestAddVerificationMethod(t *testing.T) {
doc := didcore.Document{
Context: []string{"https://www.w3.org/ns/did/v1"},
ID: "did:example:123456789abcdefghi",
}
vm := didcore.VerificationMethod{
ID: "did:example:123456789abcdefghi#keys-1",
Type: "Ed25519VerificationKey2018",
Controller: "did:example:123456789abcdefghi",
}
doc.AddVerificationMethod(vm, didcore.Purposes("authentication"))
assert.Equal(t, 1, len(doc.VerificationMethod))
assert.Equal(t, 1, len(doc.Authentication))
assert.Equal(t, vm.ID, doc.Authentication[0])
}
func TestWoo(t *testing.T) {
doc := didcore.Document{
ID: "did:example:123456789abcdefghi",
}
doc.AddVerificationMethod(didcore.VerificationMethod{
ID: "did:example:123456789abcdefghi#keys-1",
Type: "Ed25519VerificationKey2018",
Controller: "did:example:123456789abcdefghi",
}, didcore.Purposes("authentication"))
vm, err := doc.SelectVerificationMethod(didcore.Purpose("authentication"))
assert.NoError(t, err)
assert.Equal(t, "did:example:123456789abcdefghi#keys-1", vm.ID)
}

View File

@@ -0,0 +1,99 @@
package didcore
import "context"
// MethodResolver is an interface that can be implemented for resolving specific DID methods.
// Each concrete implementation should adhere to the DID core specficiation defined here:
// https://www.w3.org/TR/did-core/#did-resolution
type MethodResolver interface {
Resolve(uri string) (ResolutionResult, error)
ResolveWithContext(ctx context.Context, uri string) (ResolutionResult, error)
}
// ResolutionResult represents the result of a DID (Decentralized Identifier)
// resolution.
//
// This class encapsulates the metadata and document information obtained as
// a result of resolving a DID. It includes the resolution metadata, the DID
// document (if available), and the document metadata.
//
// The `DidResolutionResult` can be initialized with specific metadata and
// document information, or it can be created with default values if no
// specific information is provided.
type ResolutionResult struct {
// The metadata associated with the DID resolution process.
//
// This includes information about the resolution process itself, such as any errors
// that occurred. If not provided in the constructor, it defaults to an empty object
// as per the spec
ResolutionMetadata ResolutionMetadata `json:"didResolutionMetadata,omitempty"`
// The resolved DID document, if available.
//
// This is the document that represents the resolved state of the DID. It may be `null`
// if the DID could not be resolved or if the document is not available.
Document Document `json:"didDocument"`
// The metadata associated with the DID document.
//
// This includes information about the document such as when it was created and
// any other relevant metadata. If not provided in the constructor, it defaults to an
// empty `DidDocumentMetadata`.
DocumentMetadata DocumentMetadata `json:"didDocumentMetadata,omitempty"`
}
// ResolutionResultWithError creates a Resolution Result populated with all default values and the error code provided.
func ResolutionResultWithError(errorCode string) ResolutionResult {
return ResolutionResult{
ResolutionMetadata: ResolutionMetadata{
Error: errorCode,
},
DocumentMetadata: DocumentMetadata{},
}
}
// ResolutionResultWithDocument creates a Resolution Result populated with all default values and the document provided.
func ResolutionResultWithDocument(document Document) ResolutionResult {
return ResolutionResult{
ResolutionMetadata: ResolutionMetadata{},
Document: document,
DocumentMetadata: DocumentMetadata{},
}
}
// GetError returns the error code associated with the resolution result. returns an empty string if no error code is present.
func (r *ResolutionResult) GetError() string {
return r.ResolutionMetadata.Error
}
// ResolutionMetadata is a metadata structure consisting of values relating to the results of the
// DID resolution process which typically changes between invocations of the
// resolve and resolveRepresentation functions, as it represents data about
// the resolution process itself
//
// Spec: https://www.w3.org/TR/did-core/#dfn-didresolutionmetadata
type ResolutionMetadata struct {
// The Media Type of the returned didDocumentStream. This property is
// REQUIRED if resolution is successful and if the resolveRepresentation
// function was called
ContentType string `json:"contentType,omitempty"`
// The error code from the resolution process. This property is REQUIRED
// when there is an error in the resolution process. The value of this
// property MUST be a single keyword ASCII string. The possible property
// values of this field SHOULD be registered in the
// [DID Specification Registries](https://www.w3.org/TR/did-spec-registries/#error)
Error string `json:"error,omitempty"`
}
// ResolutionError represents the error field of a ResolutionMetadata object. This struct implements error and is used to
// surface the error code from the resolution process. it is returned as the error value from resolve as a means to
// support idiomatic go error handling while also remaining spec compliant. It's worth mentioning that the spec expects
// error to be returned within ResolutionMedata. Given this, the error code is also present on ResolutionMetadata whenever
// an error occurs
// well known code values can be found here: https://www.w3.org/TR/did-spec-registries/#error
type ResolutionError struct {
Code string
}
func (e ResolutionError) Error() string {
return e.Code
}

View File

@@ -0,0 +1,139 @@
package didkey
import (
"context"
"fmt"
"olares-cli/pkg/web5/crypto"
"olares-cli/pkg/web5/crypto/dsa"
"olares-cli/pkg/web5/dids/did"
"olares-cli/pkg/web5/dids/didcore"
"olares-cli/pkg/web5/jwk"
)
// createOptions is a struct that contains all options that can be passed to [Create]
type createOptions struct {
keyManager crypto.KeyManager
algorithmID string
}
// CreateOption is a type returned by all [Create] options for variadic parameter support
type CreateOption func(o *createOptions)
// KeyManager is an option that can be passed to Create to provide a KeyManager
func KeyManager(k crypto.KeyManager) CreateOption {
return func(o *createOptions) {
o.keyManager = k
}
}
// AlgorithmID is an option that can be passed to Create to specify a specific
// cryptographic algorithm to use to generate the private key
func AlgorithmID(id string) CreateOption {
return func(o *createOptions) {
o.algorithmID = id
}
}
// Create can be used to create a new `did:jwk`. `did:jwk` is useful in scenarios where:
// - Offline resolution is preferred
// - Key rotation is not required
// - Service endpoints are not necessary
//
// Spec: https://github.com/quartzjer/did-jwk/blob/main/spec.md
func Create(opts ...CreateOption) (did.BearerDID, error) {
o := createOptions{
keyManager: crypto.NewLocalKeyManager(),
algorithmID: dsa.AlgorithmIDED25519,
}
for _, opt := range opts {
opt(&o)
}
keyMgr := o.keyManager
keyID, err := keyMgr.GeneratePrivateKey(o.algorithmID)
if err != nil {
return did.BearerDID{}, fmt.Errorf("failed to generate private key: %w", err)
}
publicJWK, _ := keyMgr.GetPublicKey(keyID)
id, err := KeyToID(publicJWK)
if err != nil {
return did.BearerDID{}, fmt.Errorf("failed to convert public key to ID: %w", err)
}
publicJWK.KID = "did:key:" + id
publicJWK.USE = "sig"
// publicJWK.ALG = "EdDSA"
didJWK := did.DID{
Method: "key",
URI: "did:key:" + id,
ID: id,
}
bearerDID := did.BearerDID{
DID: didJWK,
KeyManager: keyMgr,
Document: createDocument(didJWK, publicJWK),
}
return bearerDID, nil
}
// Resolver is a type to implement resolution
type Resolver struct{}
// ResolveWithContext the provided DID URI (must be a did:jwk) as per the wee bit of detail provided in the
// spec: https://github.com/quartzjer/did-jwk/blob/main/spec.md
func (r Resolver) ResolveWithContext(ctx context.Context, uri string) (didcore.ResolutionResult, error) {
return r.Resolve(uri)
}
// Resolve the provided DID URI (must be a did:jwk) as per the wee bit of detail provided in the
// spec: https://github.com/quartzjer/did-jwk/blob/main/spec.md
func (r Resolver) Resolve(uri string) (didcore.ResolutionResult, error) {
//
return didcore.ResolutionResult{}, nil
}
// createDocument creates a DID document from a DID URI
func createDocument(did did.DID, publicKey jwk.JWK) didcore.Document {
// Create the base document
doc := didcore.Document{
Context: []string{
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/ed25519-2020/v1",
"https://w3id.org/security/suites/x25519-2020/v1",
},
ID: did.URI,
}
// Create the key ID
keyID := fmt.Sprintf("#%s", did.ID)
// Create the verification method
vm := didcore.VerificationMethod{
ID: keyID,
Type: "Ed25519VerificationKey2020",
Controller: did.URI,
PublicKeyJwk: &publicKey,
}
// Add the verification method with all purposes
doc.AddVerificationMethod(
vm,
didcore.Purposes(
"assertionMethod",
"authentication",
"capabilityDelegation",
"capabilityInvocation",
),
)
return doc
}

View File

@@ -0,0 +1,44 @@
package didkey
import (
"encoding/base64"
"fmt"
"olares-cli/pkg/web5/jwk"
"github.com/mr-tron/base58"
"github.com/multiformats/go-varint"
)
// base58Alphabet is the alphabet used for base58btc encoding
// EncodeBase58BTC 对输入数据进行 Base58btc 编码
func EncodeBase58BTC(data []byte) string {
// 调用 base58 库进行编码
base58Encoded := base58.Encode(data)
// 添加 Base58btc 的前缀 'z'
return "z" + base58Encoded
}
// KeyToID converts a public key JWK to a did:key ID
func KeyToID(publicKey jwk.JWK) (string, error) {
// Decode the public key X value from base64url
pubKeyBytes, err := base64.RawURLEncoding.DecodeString(publicKey.X)
if err != nil {
return "", fmt.Errorf("failed to decode public key: %w", err)
}
ed25519CodecID := 0xed
// Create the multicodec prefix
prefix := varint.ToUvarint(uint64(ed25519CodecID))
// Combine the prefix and public key bytes
idBytes := make([]byte, len(prefix)+len(pubKeyBytes))
copy(idBytes, prefix)
copy(idBytes[len(prefix):], pubKeyBytes)
// Encode to base58btc
id := EncodeBase58BTC(idBytes)
return id, nil
}

View File

@@ -0,0 +1,71 @@
package dids
import (
"context"
"sync"
"olares-cli/pkg/web5/dids/did"
"olares-cli/pkg/web5/dids/didcore"
)
// Resolve resolves the provided DID URI. This function is capable of resolving
// the DID methods implemented in web5-go
func Resolve(uri string) (didcore.ResolutionResult, error) {
return getDefaultResolver().Resolve(uri)
}
// ResolveWithContext resolves the provided DID URI. This function is capable of resolving
// the DID methods implemented in web5-go
func ResolveWithContext(ctx context.Context, uri string) (didcore.ResolutionResult, error) {
return getDefaultResolver().ResolveWithContext(ctx, uri)
}
var instance *didResolver
var once sync.Once
func getDefaultResolver() *didResolver {
once.Do(func() {
instance = &didResolver{
resolvers: map[string]didcore.MethodResolver{
// "dht": diddht.DefaultResolver(),
// "jwk": didjwk.Resolver{},
// "web": didweb.Resolver{},
},
}
})
return instance
}
type didResolver struct {
resolvers map[string]didcore.MethodResolver
}
func (r *didResolver) Resolve(uri string) (didcore.ResolutionResult, error) {
did, err := did.Parse(uri)
if err != nil {
return didcore.ResolutionResultWithError("invalidDid"), didcore.ResolutionError{Code: "invalidDid"}
}
resolver := r.resolvers[did.Method]
if resolver == nil {
return didcore.ResolutionResultWithError("methodNotSupported"), didcore.ResolutionError{Code: "methodNotSupported"}
}
return resolver.Resolve(uri)
}
func (r *didResolver) ResolveWithContext(ctx context.Context, uri string) (didcore.ResolutionResult, error) {
did, err := did.Parse(uri)
if err != nil {
return didcore.ResolutionResultWithError("invalidDid"), didcore.ResolutionError{Code: "invalidDid"}
}
resolver := r.resolvers[did.Method]
if resolver == nil {
return didcore.ResolutionResultWithError("methodNotSupported"), didcore.ResolutionError{Code: "methodNotSupported"}
}
return resolver.ResolveWithContext(ctx, uri)
}

46
cli/pkg/web5/jwk/jwk.go Normal file
View File

@@ -0,0 +1,46 @@
// Package jwk implements a subset of the JSON Web Key spec (https://tools.ietf.org/html/rfc7517)
package jwk
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
)
// JWK represents a JSON Web Key as per RFC7517 (https://tools.ietf.org/html/rfc7517)
// Note that this is a subset of the spec. There are a handful of properties that the
// spec allows for that are not represented here at the moment. This is because we
// only need a subset of the spec for our purposes.
type JWK struct {
ALG string `json:"alg,omitempty"`
KID string `json:"kid,omitempty"`
USE string `json:"use,omitempty"`
KTY string `json:"kty,omitempty"`
CRV string `json:"crv,omitempty"`
D string `json:"d,omitempty"`
X string `json:"x,omitempty"`
Y string `json:"y,omitempty"`
}
// ComputeThumbprint computes the JWK thumbprint as per RFC7638 (https://tools.ietf.org/html/rfc7638)
func (j JWK) ComputeThumbprint() (string, error) {
thumbprintPayload := map[string]interface{}{
"crv": j.CRV,
"kty": j.KTY,
"x": j.X,
}
if j.Y != "" {
thumbprintPayload["y"] = j.Y
}
bytes, err := json.Marshal(thumbprintPayload)
if err != nil {
return "", err
}
digest := sha256.Sum256(bytes)
thumbprint := base64.RawURLEncoding.EncodeToString(digest[:])
return thumbprint, nil
}

View File

@@ -0,0 +1 @@
package jwk_test

149
cli/pkg/web5/jws/README.md Normal file
View File

@@ -0,0 +1,149 @@
# `jws` <!-- omit in toc -->
# Table of Contents <!-- omit in toc -->
- [Features](#features)
- [Usage](#usage)
- [Signing:](#signing)
- [Detached Content](#detached-content)
- [Verifying](#verifying)
- [Directory Structure](#directory-structure)
- [Rationale](#rationale)
# Features
* Signing a JWS (JSON Web Signature) with a DID
* Verifying a JWS with a DID
# Usage
## Signing:
```go
package main
import (
"fmt"
"github.com/beclab/Olares/cli/pkg/web5/didjwk"
"github.com/beclab/Olares/cli/pkg/web5/jws"
)
func main() {
did, err := didjwk.Create()
if err != nil {
fmt.Printf("failed to create did: %v", err)
return
}
payload := map[string]interface{}{"hello": "world"}
compactJWS, err := jws.Sign(payload, did)
if err != nil {
fmt.Printf("failed to sign: %v", err)
return
}
fmt.Printf("compact JWS: %s", compactJWS)
}
```
## Detached Content
returning a JWS with detached content can be done like so:
```go
package main
import (
"fmt"
"github.com/beclab/Olares/cli/pkg/web5/didjwk"
"github.com/beclab/Olares/cli/pkg/web5/jws"
)
func main() {
did, err := didjwk.Create()
if err != nil {
fmt.Printf("failed to create did: %v", err)
return
}
payload := map[string]interface{}{"hello": "world"}
compactJWS, err := jws.Sign(payload, did, Detached(true))
if err != nil {
fmt.Printf("failed to sign: %v", err)
return
}
fmt.Printf("compact JWS: %s", compactJWS)
}
```
specifying a specific category of key associated with the provided did to sign with can be done like so:
```go
package main
import (
"fmt"
"github.com/beclab/Olares/cli/pkg/web5/didjwk"
"github.com/beclab/Olares/cli/pkg/web5/jws"
)
func main() {
bearerDID, err := didjwk.Create()
if err != nil {
fmt.Printf("failed to create did: %v", err)
return
}
payload := map[string]interface{}{"hello": "world"}
compactJWS, err := jws.Sign(payload, did, Purpose("authentication"))
if err != nil {
fmt.Printf("failed to sign: %v", err)
}
fmt.Printf("compact JWS: %s", compactJWS)
}
```
## Verifying
```go
package main
import (
"fmt"
"github.com/beclab/Olares/cli/pkg/web5/didjwk"
"github.com/beclab/Olares/cli/pkg/web5/jws"
)
func main() {
compactJWS := "SOME_JWS"
ok, err := jws.Verify(compactJWS)
if (err != nil) {
fmt.Printf("failed to verify JWS: %v", 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

View File

@@ -0,0 +1,224 @@
package jws
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/beclab/Olares/cli/pkg/web5/crypto/dsa"
"github.com/beclab/Olares/cli/pkg/web5/dids/didcore"
"github.com/syndtr/goleveldb/leveldb"
)
const (
DIDGateURL = "https://did-gate-v3.bttcdn.com/1.0/name/"
DIDGateTimeout = 10 * time.Second
)
var (
db *leveldb.DB
)
func init() {
var (
err error
info os.FileInfo
)
info, err = os.Stat("/var/lib/olares")
if os.IsNotExist(err) {
// Create the directory if it doesn't exist
if err := os.MkdirAll("/var/lib/olares", 0755); err != nil {
panic(fmt.Sprintf("failed to create directory: %v", err))
}
}
if err != nil {
panic(fmt.Sprintf("failed to check directory: %v", err))
}
if info.IsDir() == false {
err = os.Remove("/var/lib/olares")
if err != nil {
panic(fmt.Sprintf("failed to remove file: %v", err))
}
err = os.MkdirAll("/var/lib/olares", 0755)
if err != nil {
panic(fmt.Sprintf("failed to create directory: %v", err))
}
}
db, err = leveldb.OpenFile("/var/lib/olares/did_cache.db", nil)
if err != nil {
// If file exists but can't be opened, try to remove it
if os.IsExist(err) {
os.Remove("did_cache.db")
}
// Try to create a new database
db, err = leveldb.OpenFile("did_cache.db", nil)
if err != nil {
panic(fmt.Sprintf("failed to create leveldb: %v", err))
}
}
}
// CheckJWSResult represents the result of checking a JWS
type CheckJWSResult struct {
OlaresID string `json:"olares_id"`
Body interface{} `json:"body"`
KID string `json:"kid"`
}
// resolveDID resolves a DID either from cache or from the DID gate
func resolveDID(olares_id string) (*didcore.ResolutionResult, error) {
name := strings.Replace(olares_id, "@", ".", -1)
// Try to get from cache first
cached, err := db.Get([]byte(name), nil)
if err == nil {
var result didcore.ResolutionResult
if err := json.Unmarshal(cached, &result); err == nil {
return &result, nil
}
}
// If not in cache, fetch from DID gate
client := &http.Client{
Timeout: DIDGateTimeout,
}
resp, err := client.Get(DIDGateURL + name)
if err != nil {
return nil, fmt.Errorf("failed to fetch DID from gate: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("DID gate returned status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
var result didcore.ResolutionResult
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse DID document: %w", err)
}
// Cache the result
if err := db.Put([]byte(name), body, nil); err != nil {
// Log error but don't fail
fmt.Printf("failed to cache DID document: %v\n", err)
}
return &result, nil
}
// CheckJWS verifies a JWS and returns the terminus name, body and kid
func CheckJWS(jws string, duration int64) (*CheckJWSResult, error) {
var kid string
var name string
var timestamp int64
// Split JWS into segments
segs := strings.Split(jws, ".")
if len(segs) != 3 {
return nil, fmt.Errorf("invalid jws: wrong number of segments")
}
// Parse header
headerBytes, err := base64.RawURLEncoding.DecodeString(segs[0])
if err != nil {
return nil, fmt.Errorf("invalid jws: failed to decode header: %w", err)
}
var header struct {
KID string `json:"kid"`
}
if err := json.Unmarshal(headerBytes, &header); err != nil {
return nil, fmt.Errorf("invalid jws: failed to parse header: %w", err)
}
kid = header.KID
// Parse payload
payloadBytes, err := base64.RawURLEncoding.DecodeString(segs[1])
if err != nil {
return nil, fmt.Errorf("invalid jws: failed to decode payload: %w", err)
}
var payload struct {
DID string `json:"did"`
Name string `json:"name"`
Time string `json:"time"`
Domain string `json:"domain"`
Challenge string `json:"challenge"`
Body map[string]interface{} `json:"body"`
}
if err := json.Unmarshal(payloadBytes, &payload); err != nil {
return nil, fmt.Errorf("invalid jws: failed to parse payload: %w", err)
}
name = payload.Name
// Convert time string to int64
timestamp, err = strconv.ParseInt(payload.Time, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid time format: %w", err)
}
// Validate required fields
if name == "" || kid == "" || timestamp == 0 {
return nil, fmt.Errorf("invalid jws: missing required fields")
}
// Check timestamp
now := time.Now().UnixMilli()
if now-timestamp > duration {
return nil, fmt.Errorf("timestamp is out of range")
}
// Resolve DID
resolutionResult, err := resolveDID(name)
if err != nil {
return nil, fmt.Errorf("failed to resolve DID: %w", err)
}
// Verify DID matches
if resolutionResult.Document.ID != kid {
return nil, fmt.Errorf("DID does not match")
}
// Get verification method
if len(resolutionResult.Document.VerificationMethod) == 0 || resolutionResult.Document.VerificationMethod[0].PublicKeyJwk == nil {
return nil, fmt.Errorf("invalid DID document: missing verification method")
}
// Verify signature
toVerify := segs[0] + "." + segs[1]
signature, err := base64.RawURLEncoding.DecodeString(segs[2])
if err != nil {
return nil, fmt.Errorf("invalid jws: failed to decode signature: %w", err)
}
verified, err := dsa.Verify([]byte(toVerify), signature, *resolutionResult.Document.VerificationMethod[0].PublicKeyJwk)
if err != nil {
return nil, fmt.Errorf("failed to verify signature: %w", err)
}
if !verified {
return nil, fmt.Errorf("invalid signature")
}
result := CheckJWSResult{
OlaresID: name,
Body: payload,
KID: kid,
}
return &result, nil
}

306
cli/pkg/web5/jws/jws.go Normal file
View File

@@ -0,0 +1,306 @@
package jws
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"olares-cli/pkg/web5/crypto/dsa"
"olares-cli/pkg/web5/dids"
_did "olares-cli/pkg/web5/dids/did"
"olares-cli/pkg/web5/dids/didcore"
)
// Decode decodes the given JWS string into a [Decoded] type
//
// # Note
//
// The given JWS input is assumed to be a [compact JWS]
//
// [compact JWS]: https://datatracker.ietf.org/doc/html/rfc7515#section-7.1
func Decode(jws string, opts ...DecodeOption) (Decoded, error) {
o := decodeOptions{}
for _, opt := range opts {
opt(&o)
}
parts := strings.Split(jws, ".")
if len(parts) != 3 {
return Decoded{}, fmt.Errorf("malformed JWS. Expected 3 parts, got %d", len(parts))
}
header, err := DecodeHeader(parts[0])
if err != nil {
return Decoded{}, fmt.Errorf("malformed JWS. Failed to decode header: %w", err)
}
var payload []byte
if o.payload == nil {
payload, err = base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return Decoded{}, fmt.Errorf("malformed JWS. Failed to decode payload: %w", err)
}
} else {
payload = o.payload
parts[1] = base64.RawURLEncoding.EncodeToString(payload)
}
signature, err := DecodeSignature(parts[2])
if err != nil {
return Decoded{}, fmt.Errorf("malformed JWS. Failed to decode signature: %w", err)
}
if header.KID == "" {
return Decoded{}, errors.New("malformed JWS. Expected header to contain kid")
}
signerDID, err := _did.Parse(header.KID)
if err != nil {
return Decoded{}, fmt.Errorf("malformed JWS. Failed to parse kid: %w", err)
}
return Decoded{
Header: header,
Payload: payload,
Signature: signature,
Parts: parts,
SignerDID: signerDID,
}, nil
}
type decodeOptions struct {
payload []byte
}
// DecodeOption represents an option that can be passed to [Decode] or [Verify].
type DecodeOption func(opts *decodeOptions)
// Payload can be passed to [Decode] or [Verify] to provide a detached payload.
// More info on detached payloads can be found [here].
//
// [here]: https://datatracker.ietf.org/doc/html/rfc7515#appendix-F
func Payload(p []byte) DecodeOption {
return func(opts *decodeOptions) {
opts.payload = p
}
}
// DecodeHeader decodes the base64url encoded JWS header into a [Header]
func DecodeHeader(base64UrlEncodedHeader string) (Header, error) {
bytes, err := base64.RawURLEncoding.DecodeString(base64UrlEncodedHeader)
if err != nil {
return Header{}, err
}
var header Header
err = json.Unmarshal(bytes, &header)
if err != nil {
return Header{}, err
}
return header, nil
}
// DecodeSignature decodes the base64url encoded JWS signature into a byte array
func DecodeSignature(base64UrlEncodedSignature string) ([]byte, error) {
signature, err := base64.RawURLEncoding.DecodeString(base64UrlEncodedSignature)
if err != nil {
return nil, err
}
return signature, nil
}
// options that sign function can take
type signOpts struct {
selector didcore.VMSelector
detached bool
typ string
}
// SignOpt is a type that represents an option that can be passed to [olares/olares-cli/pkg/web5/jws.Sign].
type SignOpt func(opts *signOpts)
// Purpose is an option that can be passed to [olares/olares-cli/pkg/web5/jws.Sign].
// It is used to select the appropriate key to sign with
func Purpose(p string) SignOpt {
return func(opts *signOpts) {
opts.selector = didcore.Purpose(p)
}
}
// VerificationMethod is an option that can be passed to [olares/olares-cli/pkg/web5/jws.Sign].
// It is used to select the appropriate key to sign with
func VerificationMethod(id string) SignOpt {
return func(opts *signOpts) {
opts.selector = didcore.ID(id)
}
}
// VMSelector is an option that can be passed to [olares/olares-cli/pkg/web5/jws.Sign].
// It is used to select the appropriate key to sign with
func VMSelector(selector didcore.VMSelector) SignOpt {
return func(opts *signOpts) {
opts.selector = selector
}
}
// DetachedPayload is an option that can be passed to [olares/olares-cli/pkg/web5/jws.Sign].
// It is used to indicate whether the payload should be included in the signature.
// More details can be found [here].
//
// [Specification]: https://datatracker.ietf.org/doc/html/rfc7515#appendix-F
func DetachedPayload(detached bool) SignOpt {
return func(opts *signOpts) {
opts.detached = detached
}
}
// Type is an option that can be passed to [olares/olares-cli/pkg/web5/jws.Sign].
// It is used to set the `typ` JWS header value
func Type(typ string) SignOpt {
return func(opts *signOpts) {
opts.typ = typ
}
}
// Sign signs the provided payload with a key associated to the provided DID.
// if no purpose is provided, the default is "assertionMethod". Passing Detached(true)
// will return a compact JWS with detached content
func Sign(payload []byte, did _did.BearerDID, opts ...SignOpt) (string, error) {
o := signOpts{selector: nil, detached: false}
for _, opt := range opts {
opt(&o)
}
sign, verificationMethod, err := did.GetSigner(o.selector)
if err != nil {
return "", fmt.Errorf("failed to get signer: %w", err)
}
jwa, err := dsa.GetJWA(*verificationMethod.PublicKeyJwk)
if err != nil {
return "", fmt.Errorf("failed to determine alg: %w", err)
}
keyID := did.Document.GetAbsoluteResourceID(verificationMethod.ID)
header := Header{ALG: jwa, KID: keyID, TYP: o.typ}
base64UrlEncodedHeader, err := header.Encode()
if err != nil {
return "", fmt.Errorf("failed to base64 url encode header: %w", err)
}
base64UrlEncodedPayload := base64.RawURLEncoding.EncodeToString(payload)
toSign := base64UrlEncodedHeader + "." + base64UrlEncodedPayload
toSignBytes := []byte(toSign)
signature, err := sign(toSignBytes)
if err != nil {
return "", fmt.Errorf("failed to compute signature: %w", err)
}
base64UrlEncodedSignature := base64.RawURLEncoding.EncodeToString(signature)
var compactJWS string
if o.detached {
compactJWS = base64UrlEncodedHeader + "." + "." + base64UrlEncodedSignature
} else {
compactJWS = toSign + "." + base64UrlEncodedSignature
}
return compactJWS, nil
}
// Verify verifies the given compactJWS by resolving the DID Document from the kid header value
// and using the associated public key found by resolving the DID Document
func Verify(compactJWS string, opts ...DecodeOption) (Decoded, error) {
decodedJWS, err := Decode(compactJWS, opts...)
if err != nil {
return decodedJWS, fmt.Errorf("signature verification failed: %w", err)
}
err = decodedJWS.Verify()
return decodedJWS, err
}
// Decoded is a compact JWS decoded into its parts
type Decoded struct {
Header Header
Payload []byte
Signature []byte
Parts []string
SignerDID _did.DID
}
// Verify verifies the given compactJWS by resolving the DID Document from the kid header value
// and using the associated public key found by resolving the DID Document
func (jws Decoded) Verify() error {
if jws.Header.ALG == "" || jws.Header.KID == "" {
return errors.New("malformed JWS header. alg and kid are required")
}
did, err := _did.Parse(jws.Header.KID)
if err != nil {
return errors.New("malformed JWS header. kid must be a DID URL")
}
resolutionResult, err := dids.Resolve(did.URI)
if err != nil {
return fmt.Errorf("failed to resolve DID: %w", err)
}
vmSelector := didcore.ID(did.URL())
verificationMethod, err := resolutionResult.Document.SelectVerificationMethod(vmSelector)
if err != nil {
return fmt.Errorf("kid does not match any verification method %w", err)
}
toVerify := jws.Parts[0] + "." + jws.Parts[1]
verified, err := dsa.Verify([]byte(toVerify), jws.Signature, *verificationMethod.PublicKeyJwk)
if err != nil {
return fmt.Errorf("failed to verify signature: %w", err)
}
if !verified {
return errors.New("invalid signature")
}
return nil
}
// Header represents a JWS (JSON Web Signature) header. See [Specification] for more details.
// [Specification]: https://datatracker.ietf.org/doc/html/rfc7515#section-4
type Header struct {
// Ide ntifies the cryptographic algorithm used to secure the JWS. The JWS Signature value is not
// valid if the "alg" value does not represent a supported algorithm or if there is not a key for
// use with that algorithm associated with the party that digitally signed or MACed the content.
//
// "alg" values should either be registered in the IANA "JSON Web Signature and Encryption
// Algorithms" registry or be a value that contains a Collision-Resistant Name. The "alg" value is
// a case-sensitive ASCII string. This Header Parameter MUST be present and MUST be understood
// and processed by implementations.
//
// [Specification]: https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.1
ALG string `json:"alg,omitempty"`
// Key ID Header Parameter https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.4
KID string `json:"kid,omitempty"`
// Type Header Parameter https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.9
TYP string `json:"typ,omitempty"`
}
// Encode returns the base64url encoded header.
func (j Header) Encode() (string, error) {
bytes, err := json.Marshal(j)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(bytes), nil
}

View File

@@ -0,0 +1,299 @@
package jws_test
import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"testing"
"olares-cli/pkg/web5/dids/didkey"
"olares-cli/pkg/web5/jws"
"github.com/alecthomas/assert/v2"
)
func TestDecode(t *testing.T) {
did, err := didkey.Create()
assert.NoError(t, err)
payload := []byte("hi")
compactJWS, err := jws.Sign(payload, did)
assert.NoError(t, err)
decoded, err := jws.Decode(compactJWS)
assert.NoError(t, err)
assert.Equal(t, payload, decoded.Payload)
}
func TestDecode_SuccessWithTestJwtWithPayload(t *testing.T) {
jwsString := "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaU" +
"xDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbWRsWjI5YWNuWTVjemxuVWtwT1praFBlVGt5Tm" +
"1oa1drNTBVMWxZWjJoaFlsOVJSbWhGTlRNM1lrMGlmUSMwIiwidHlwIjoiSldUIn0" +
".eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2ll" +
"Q0k2SW1kbFoyOWFjblk1Y3psblVrcE9aa2hQZVRreU5taGtXazUwVTFsWVoyaGhZbDlSUm1oRk5UTT" +
"NZazBpZlEiLCJqdGkiOiJ1cm46dmM6dXVpZDpjNWMzZGExMi02ODhmLTQxZDYtOTQzMC1lYzViNDAy" +
"NTFmMzMiLCJuYmYiOjE3MTE2NTA4MjcsInN1YiI6IjEyMyIsInZjIjp7IkBjb250ZXh0IjpbImh0dHB" +
"zOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZW" +
"RlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKamNuWWlPaUpGWkRJMU" +
"5URTVJaXdpZUNJNkltZGxaMjlhY25ZNWN6bG5Va3BPWmtoUGVUa3lObWhrV2s1MFUxbFlaMmhoWWw5UlJ" +
"taEZOVE0zWWswaWZRIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiIxMjMifSwiaWQiOiJ1cm46dmM" +
"6dXVpZDpjNWMzZGExMi02ODhmLTQxZDYtOTQzMC1lYzViNDAyNTFmMzMiLCJpc3N1YW5jZURhdGUiOiIy" +
"MDI0LTAzLTI4VDE4OjMzOjQ3WiJ9fQ" +
".ydUiwf33dDCdk4RyPfoTdgbK3yTUpLCDpPBIECbn-rCGn_W3q5QxzAt43ClOIWibpOXHs-9T86UDBFPyd79vAQ"
decoded, err := jws.Decode(jwsString)
assert.NoError(t, err)
assert.Equal(t, "EdDSA", decoded.Header.ALG)
assert.Equal(t, "JWT", decoded.Header.TYP)
assert.Equal(t, "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Imdl"+
"Z29acnY5czlnUkpOZkhPeTkyNmhkWk50U1lYZ2hhYl9RRmhFNTM3Yk0ifQ", decoded.SignerDID.URI)
var payloadMap map[string]interface{}
err = json.Unmarshal(decoded.Payload, &payloadMap)
assert.NoError(t, err)
if iss, ok := payloadMap["iss"].(string); ok {
assert.Equal(t, "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Imdl"+
"Z29acnY5czlnUkpOZkhPeTkyNmhkWk50U1lYZ2hhYl9RRmhFNTM3Yk0ifQ", iss)
} else {
t.Fail()
}
if subject, ok := payloadMap["sub"].(string); ok {
assert.Equal(t, "123", subject)
} else {
t.Fail()
}
if notBefore, ok := payloadMap["nbf"].(float64); ok {
assert.Equal(t, 1711650827, notBefore)
} else {
t.Fail()
}
}
func TestDecode_SuccessWithTestJwtWithDetachedPayload(t *testing.T) {
jwsString := "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaU" +
"xDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbWRsWjI5YWNuWTVjemxuVWtwT1praFBlVGt5Tm" +
"1oa1drNTBVMWxZWjJoaFlsOVJSbWhGTlRNM1lrMGlmUSMwIiwidHlwIjoiSldUIn0" +
"..ydUiwf33dDCdk4RyPfoTdgbK3yTUpLCDpPBIECbn-rCGn_W3q5QxzAt43ClOIWibpOXHs-9T86UDBFPyd79vAQ"
payloadBase64Url := "eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2ll" +
"Q0k2SW1kbFoyOWFjblk1Y3psblVrcE9aa2hQZVRreU5taGtXazUwVTFsWVoyaGhZbDlSUm1oRk5UTT" +
"NZazBpZlEiLCJqdGkiOiJ1cm46dmM6dXVpZDpjNWMzZGExMi02ODhmLTQxZDYtOTQzMC1lYzViNDAy" +
"NTFmMzMiLCJuYmYiOjE3MTE2NTA4MjcsInN1YiI6IjEyMyIsInZjIjp7IkBjb250ZXh0IjpbImh0dHB" +
"zOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZW" +
"RlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKamNuWWlPaUpGWkRJMU" +
"5URTVJaXdpZUNJNkltZGxaMjlhY25ZNWN6bG5Va3BPWmtoUGVUa3lObWhrV2s1MFUxbFlaMmhoWWw5UlJ" +
"taEZOVE0zWWswaWZRIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiIxMjMifSwiaWQiOiJ1cm46dmM" +
"6dXVpZDpjNWMzZGExMi02ODhmLTQxZDYtOTQzMC1lYzViNDAyNTFmMzMiLCJpc3N1YW5jZURhdGUiOiIy" +
"MDI0LTAzLTI4VDE4OjMzOjQ3WiJ9fQ"
payloadByteArray, err := base64.StdEncoding.DecodeString(payloadBase64Url)
if err != nil {
fmt.Println("Error decoding base64 string:", err)
return
}
decoded, err := jws.Decode(jwsString, jws.Payload(payloadByteArray))
assert.NoError(t, err)
assert.Equal(t, "EdDSA", decoded.Header.ALG)
assert.Equal(t, "JWT", decoded.Header.TYP)
assert.Equal(t, "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Imdl"+
"Z29acnY5czlnUkpOZkhPeTkyNmhkWk50U1lYZ2hhYl9RRmhFNTM3Yk0ifQ", decoded.SignerDID.URI)
var payloadMap map[string]interface{}
err = json.Unmarshal(decoded.Payload, &payloadMap)
assert.NoError(t, err)
if iss, ok := payloadMap["iss"].(string); ok {
assert.Equal(t, "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Imdl"+
"Z29acnY5czlnUkpOZkhPeTkyNmhkWk50U1lYZ2hhYl9RRmhFNTM3Yk0ifQ", iss)
} else {
t.Fail()
}
if subject, ok := payloadMap["sub"].(string); ok {
assert.Equal(t, "123", subject)
} else {
t.Fail()
}
if notBefore, ok := payloadMap["nbf"].(float64); ok {
assert.Equal(t, 1711650827, notBefore)
} else {
t.Fail()
}
}
func TestDecode_HeaderIsNotBase64Url(t *testing.T) {
compactJWS := "lol." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." +
"SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
decoded, err := jws.Decode(compactJWS)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Failed to decode header")
assert.Equal(t, jws.Decoded{}, decoded)
}
func TestDecode_PayloadIsNotBase64Url(t *testing.T) {
compactJWS := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +
"{woohoo}." +
"SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
decoded, err := jws.Decode(compactJWS)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Failed to decode payload")
assert.Equal(t, jws.Decoded{}, decoded)
}
func TestDecode_SignatureIsNotBase64Url(t *testing.T) {
compactJWS := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." +
"{woot}"
decoded, err := jws.Decode(compactJWS)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Failed to decode signature")
assert.Equal(t, jws.Decoded{}, decoded)
}
func TestDecode_MissingHeaderKid(t *testing.T) {
compactJWS := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." +
"SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
decoded, err := jws.Decode(compactJWS)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Expected header to contain kid")
assert.Equal(t, jws.Decoded{}, decoded)
}
func TestDecode_Bad(t *testing.T) {
badHeader := base64.RawURLEncoding.EncodeToString([]byte("hehe"))
vectors := []string{
"",
"..",
"a.b.c",
fmt.Sprintf("%s.%s.%s", badHeader, badHeader, badHeader),
}
for _, vector := range vectors {
decoded, err := jws.Decode(vector)
assert.Error(t, err, "expected verification error. vector: %s", vector)
assert.Equal(t, jws.Decoded{}, decoded, "expected empty DecodedJWS")
}
}
func TestSign_Detached(t *testing.T) {
did, err := didkey.Create()
assert.NoError(t, err)
payload := map[string]any{"hello": "world"}
payloadBytes, err := json.Marshal(payload)
assert.NoError(t, err)
compactJWS, err := jws.Sign(payloadBytes, did, jws.DetachedPayload(true))
assert.NoError(t, err)
assert.True(t, compactJWS != "", "expected signature to be non-empty")
parts := strings.Split(compactJWS, ".")
assert.Equal(t, 3, len(parts), "expected 3 parts in compact JWS")
assert.Equal(t, parts[1], "", "expected empty payload")
}
func TestSign_CustomType(t *testing.T) {
did, err := didkey.Create()
assert.NoError(t, err)
payload := map[string]any{"hello": "world"}
payloadBytes, err := json.Marshal(payload)
assert.NoError(t, err)
customType := "openid4vci-proof+jwt"
compactJWS, err := jws.Sign(payloadBytes, did, jws.Type(customType))
assert.NoError(t, err)
parts := strings.Split(compactJWS, ".")
encodedHeader := parts[0]
header, err := jws.DecodeHeader(encodedHeader)
assert.NoError(t, err)
assert.Equal(t, customType, header.TYP)
}
func TestDecoded_Verify(t *testing.T) {
did, err := didkey.Create()
assert.NoError(t, err)
payload := map[string]any{"hello": "world"}
payloadBytes, err := json.Marshal(payload)
assert.NoError(t, err)
compactJWS, err := jws.Sign(payloadBytes, did)
assert.NoError(t, err)
decoded, err := jws.Decode(compactJWS)
assert.NoError(t, err)
assert.NotEqual(t, jws.Decoded{}, decoded, "expected decoded to not be empty")
}
func TestDecoded_Verify_Bad(t *testing.T) {
did, err := didkey.Create()
assert.NoError(t, err)
header, err := jws.Header{
ALG: "ES256K",
KID: did.Document.VerificationMethod[0].ID,
}.Encode()
assert.NoError(t, err)
payloadJSON := map[string]any{"hello": "world"}
payloadBytes, _ := json.Marshal(payloadJSON)
payload := base64.RawURLEncoding.EncodeToString(payloadBytes)
compactJWS := fmt.Sprintf("%s.%s.%s", header, payload, payload)
_, err = jws.Verify(compactJWS)
assert.Error(t, err)
assert.Contains(t, err.Error(), "signature")
}
func TestVerify(t *testing.T) {
did, err := didkey.Create()
assert.NoError(t, err)
payload := map[string]any{"hello": "world"}
payloadBytes, err := json.Marshal(payload)
assert.NoError(t, err)
compactJWS, err := jws.Sign(payloadBytes, did)
assert.NoError(t, err)
_, err = jws.Verify(compactJWS)
assert.NoError(t, err)
}
func TestVerify_Detached(t *testing.T) {
did, err := didkey.Create()
assert.NoError(t, err)
payload := []byte("hi")
compactJWS, err := jws.Sign(payload, did, jws.DetachedPayload(true))
assert.NoError(t, err)
decoded, err := jws.Verify(compactJWS, jws.Payload(payload))
assert.NoError(t, err)
assert.Equal(t, payload, decoded.Payload)
}

View File

@@ -0,0 +1,79 @@
# `jwt` <!-- omit in toc -->
# Table of Contents
- [Table of Contents](#table-of-contents)
- [Usage](#usage)
- [Signing](#signing)
- [Verifying](#verifying)
- [Directory Structure](#directory-structure)
- [Rationale](#rationale)
# Usage
## Signing
```go
package main
import (
"fmt"
"github.com/beclab/Olares/cli/pkg/web5/didjwk"
"github.com/beclab/Olares/cli/pkg/web5/jwt"
)
func main() {
did, err := didjwk.Create()
if err != nil {
panic(err)
}
claims := jwt.Claims{
Issuer: did.URI,
Misc: map[string]interface{}{"c_nonce": "abcd123"},
}
jwt, err := jwt.Sign(claims, did)
if err != nil {
panic(err)
}
}
```
## Verifying
```go
package main
import (
"fmt"
"github.com/beclab/Olares/cli/pkg/web5/dids"
"github.com/beclab/Olares/cli/pkg/web5/jwt"
)
func main() {
someJWT := "SOME_JWT"
ok, err := jwt.Verify(signedJWT)
if err != nil {
panic(err)
}
if (!ok) {
fmt.Printf("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`.

286
cli/pkg/web5/jwt/jwt.go Normal file
View File

@@ -0,0 +1,286 @@
package jwt
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"olares-cli/pkg/web5/dids/did"
"olares-cli/pkg/web5/dids/didcore"
"olares-cli/pkg/web5/jws"
)
// Decode decodes the 3-part base64url encoded jwt into it's relevant parts
func Decode(jwt string) (Decoded, error) {
parts := strings.Split(jwt, ".")
if len(parts) != 3 {
return Decoded{}, fmt.Errorf("malformed JWT. Expected 3 parts, got %d", len(parts))
}
header, err := jws.DecodeHeader(parts[0])
if err != nil {
return Decoded{}, fmt.Errorf("malformed JWT. Failed to decode header: %w", err)
}
claimsBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return Decoded{}, fmt.Errorf("malformed JWT. Failed to decode claims: %w", err)
}
claims := Claims{}
err = json.Unmarshal(claimsBytes, &claims)
if err != nil {
return Decoded{}, fmt.Errorf("malformed JWT. Failed to unmarshal claims: %w", err)
}
signature, err := jws.DecodeSignature(parts[2])
if err != nil {
return Decoded{}, fmt.Errorf("malformed JWT. Failed to decode signature: %w", err)
}
signerDid, err := did.Parse(header.KID)
if err != nil {
return Decoded{}, fmt.Errorf("malformed JWT. Failed to parse signer DID: %w", err)
}
return Decoded{
Header: header,
Claims: claims,
Signature: signature,
Parts: parts,
SignerDID: signerDid,
}, nil
}
// signOpts is a type that holds all the options that can be passed to Sign
type signOpts struct {
selector didcore.VMSelector
typ string
}
// SignOpt is a type returned by all individual Sign Options.
type SignOpt func(opts *signOpts)
// Purpose is an option that can be provided to Sign to specify that a key from
// a given DID Document Verification Relationship should be used (e.g. authentication)
// Purpose is an option that can be passed to [olares/olares-cli/pkg/web5/jws.Sign].
// It is used to select the appropriate key to sign with
func Purpose(p string) SignOpt {
return func(opts *signOpts) {
opts.selector = didcore.Purpose(p)
}
}
// Type is an option that can be used to set the typ header of the JWT
func Type(t string) SignOpt {
return func(opts *signOpts) {
opts.typ = t
}
}
// Sign signs the provided JWT Claims with the provided BearerDID.
// The Purpose option can be provided to specify that a key from a given
// DID Document Verification Relationship should be used (e.g. authentication).
// defaults to using assertionMethod
//
// # Note
//
// claims.Issuer will be overridden to the value of did.URI within this function
func Sign(claims Claims, did did.BearerDID, opts ...SignOpt) (string, error) {
o := signOpts{selector: nil, typ: ""}
for _, opt := range opts {
opt(&o)
}
jwsOpts := make([]jws.SignOpt, 0)
if o.typ != "" {
jwsOpts = append(jwsOpts, jws.Type(o.typ))
}
if o.selector != nil {
jwsOpts = append(jwsOpts, jws.VMSelector(o.selector))
}
// `iss` is required to be equal to the DID's URI
claims.Issuer = did.URI
payload, err := json.Marshal(claims)
if err != nil {
return "", fmt.Errorf("failed to marshal jwt claims: %w", err)
}
return jws.Sign(payload, did, jwsOpts...)
}
// Verify verifies a JWT (JSON Web Token) as per the spec https://datatracker.ietf.org/doc/html/rfc7519
// Successful verification means that the JWT has not expired and the signature's integrity is intact
// Decoded JWT is returned if verification is successful
func Verify(jwt string) (Decoded, error) {
decodedJWT, err := Decode(jwt)
if err != nil {
return Decoded{}, err
}
err = decodedJWT.Verify()
return decodedJWT, err
}
// Header are JWS Headers. type aliasing because this could cause confusion
// for non-neckbeards
type Header = jws.Header
// Decoded represents a JWT Decoded into it's relevant parts
type Decoded struct {
Header Header
Claims Claims
Signature []byte
Parts []string
SignerDID did.DID
}
// Verify verifies a JWT (JSON Web Token)
func (jwt Decoded) Verify() error {
if jwt.Claims.Expiration != 0 && time.Now().Unix() > jwt.Claims.Expiration {
return errors.New("JWT has expired")
}
claimsBytes, err := base64.RawURLEncoding.DecodeString(jwt.Parts[1])
if err != nil {
return fmt.Errorf("malformed JWT. Failed to decode claims: %w", err)
}
decodedJWS := jws.Decoded{
Header: jwt.Header,
Payload: claimsBytes,
Signature: jwt.Signature,
Parts: jwt.Parts,
}
err = decodedJWS.Verify()
if err != nil {
return fmt.Errorf("JWT signature verification failed: %w", err)
}
// check to ensure that issuer has been set and that it matches the did used to sign.
// the value of KID should always be ${did}#${verificationMethodID} (aka did url)
if jwt.Claims.Issuer == "" || !strings.HasPrefix(jwt.Header.KID, jwt.Claims.Issuer) {
return errors.New("JWT issuer does not match the did url provided as KID")
}
//! we should check ^ prior to verifying the signature as verification
//! requires DID resolution which is a network call. doing so without duplicating
//! code is a bit tricky (Moe 2024-02-25)
return nil
}
// Claims represents JWT (JSON Web Token) Claims
//
// Spec: https://datatracker.ietf.org/doc/html/rfc7519#section-4
type Claims struct {
// The "iss" (issuer) claim identifies the principal that issued the
// JWT.
//
// Spec: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
Issuer string `json:"iss,omitempty"`
// The "sub" (subject) claim identifies the principal that is the
// subject of the JWT.
//
// Spec: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2
Subject string `json:"sub,omitempty"`
// The "aud" (audience) claim identifies the recipients that the JWT is
// intended for.
//
// Spec: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
Audience string `json:"aud,omitempty"`
// The "exp" (expiration time) claim identifies the expiration time on
// or after which the JWT must not be accepted for processing.
//
// Spec: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
Expiration int64 `json:"exp,omitempty"`
// The "nbf" (not before) claim identifies the time before which the JWT
// must not be accepted for processing.
//
// Spec: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
NotBefore int64 `json:"nbf,omitempty"`
// The "iat" (issued at) claim identifies the time at which the JWT was
// issued.
//
// Spec: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6
IssuedAt int64 `json:"iat,omitempty"`
// The "jti" (JWT ID) claim provides a unique identifier for the JWT.
//
// Spec: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7
JTI string `json:"jti,omitempty"`
Misc map[string]any `json:"-"`
}
// MarshalJSON overrides default json.Marshal behavior to include misc claims as flattened
// properties of the top-level object
func (c Claims) MarshalJSON() ([]byte, error) {
copied := cpy(c)
bytes, err := json.Marshal(copied)
if err != nil {
return nil, err
}
var combined map[string]interface{}
err = json.Unmarshal(bytes, &combined)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal jwt claims: %w", err)
}
// Add private claims to the map
for key, value := range c.Misc {
combined[key] = value
}
return json.Marshal(combined)
}
// UnmarshalJSON overrides default json.Unmarshal behavior to place flattened Misc
// claims into Misc
func (c *Claims) UnmarshalJSON(b []byte) error {
var m map[string]interface{}
if err := json.Unmarshal(b, &m); err != nil {
return err
}
registeredClaims := map[string]bool{
"iss": true, "sub": true, "aud": true,
"exp": true, "nbf": true, "iat": true,
"jti": true,
}
misc := make(map[string]any)
for key, value := range m {
if _, ok := registeredClaims[key]; !ok {
misc[key] = value
}
}
claims := cpy{}
if err := json.Unmarshal(b, &claims); err != nil {
return err
}
claims.Misc = misc
*c = Claims(claims)
return nil
}
// cpy is a copy of Claims that is used to marshal/unmarshal the claims without infinitely looping
type cpy Claims

View File

@@ -0,0 +1,144 @@
package jwt_test
import (
"encoding/json"
"fmt"
"testing"
"olares-cli/pkg/web5/dids/didkey"
"olares-cli/pkg/web5/jws"
"olares-cli/pkg/web5/jwt"
"github.com/alecthomas/assert/v2"
)
func TestClaims_MarshalJSON(t *testing.T) {
claims := jwt.Claims{
Issuer: "issuer",
Misc: map[string]interface{}{"foo": "bar"},
}
b, err := json.Marshal(&claims)
assert.NoError(t, err)
obj := make(map[string]interface{})
err = json.Unmarshal(b, &obj)
assert.NoError(t, err)
assert.Equal(t, "issuer", obj["iss"])
assert.False(t, obj["foo"] == nil)
}
func TestClaims_UnmarshalJSON(t *testing.T) {
claims := jwt.Claims{
Issuer: "issuer",
Misc: map[string]interface{}{"foo": "bar"},
}
b, err := json.Marshal(&claims)
assert.NoError(t, err)
claimsAgane := jwt.Claims{}
err = json.Unmarshal(b, &claimsAgane)
assert.NoError(t, err)
assert.Equal(t, claims.Issuer, claimsAgane.Issuer)
assert.False(t, claimsAgane.Misc["foo"] == nil)
assert.Equal(t, claimsAgane.Misc["foo"], claims.Misc["foo"])
}
func TestSign(t *testing.T) {
did, err := didkey.Create()
assert.NoError(t, err)
claims := jwt.Claims{
Issuer: did.ID,
Misc: map[string]interface{}{"c_nonce": "abcd123"},
}
jwt, err := jwt.Sign(claims, did)
assert.NoError(t, err)
assert.False(t, jwt == "", "expected jwt to not be empty")
}
func TestSign_IssuerOverridden(t *testing.T) {
did, err := didkey.Create()
assert.NoError(t, err)
claims := jwt.Claims{
Issuer: "something-not-equal-to-did.URI", // this will be overridden by the call to jwt.Sign()
Misc: map[string]interface{}{"c_nonce": "abcd123"},
}
signed, err := jwt.Sign(claims, did)
assert.NoError(t, err)
decoded, err := jwt.Decode(signed)
assert.NoError(t, err)
assert.Equal(t, did.URI, decoded.Claims.Issuer)
}
func TestVerify(t *testing.T) {
did, err := didkey.Create()
assert.NoError(t, err)
claims := jwt.Claims{
Issuer: did.URI,
Misc: map[string]interface{}{"c_nonce": "abcd123"},
}
signedJWT, err := jwt.Sign(claims, did)
assert.NoError(t, err)
assert.False(t, signedJWT == "", "expected jwt to not be empty")
decoded, err := jwt.Verify(signedJWT)
assert.NoError(t, err)
assert.NotEqual(t, decoded, jwt.Decoded{}, "expected decoded to not be empty")
}
func TestVerify_BadClaims(t *testing.T) {
okHeader, err := jws.Header{ALG: "ES256K", KID: "did:web:abc#key-1"}.Encode()
assert.NoError(t, err)
input := fmt.Sprintf("%s.%s.%s", okHeader, "hehe", "hehe")
decoded, err := jwt.Verify(input)
assert.Error(t, err)
assert.Equal(t, jwt.Decoded{}, decoded)
}
func Test_Decode_Empty(t *testing.T) {
decoded, err := jwt.Decode("")
assert.Error(t, err)
assert.Equal(t, jwt.Decoded{}, decoded)
}
func Test_Decode_Works(t *testing.T) {
vcJwt := `eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbkY0VjFGS2F6RTJSbWhCZWtOQlRsRktaR1F5UTFkRldrcE9lbXBSYjNGSmRYWk5SbUpVWjFKTVNFRWlmUSMwIiwidHlwIjoiSldUIn0.eyJleHAiOjE3MjQ1MzQwNTAsImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbkY0VjFGS2F6RTJSbWhCZWtOQlRsRktaR1F5UTFkRldrcE9lbXBSYjNGSmRYWk5SbUpVWjFKTVNFRWlmUSIsImp0aSI6InVybjp2Yzp1dWlkOjlkMzdmMzY3LWE4ZDctNDY4Zi05NGYwLTk1NzAxNzBkNzZhNCIsIm5iZiI6MTcyMTk0MjA1MCwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW5GNFYxRkthekUyUm1oQmVrTkJUbEZLWkdReVExZEZXa3BPZW1wUmIzRkpkWFpOUm1KVVoxSk1TRUVpZlEiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpc3N1ZXIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW5GNFYxRkthekUyUm1oQmVrTkJUbEZLWkdReVExZEZXa3BPZW1wUmIzRkpkWFpOUm1KVVoxSk1TRUVpZlEifSwiaWQiOiJ1cm46dmM6dXVpZDo5ZDM3ZjM2Ny1hOGQ3LTQ2OGYtOTRmMC05NTcwMTcwZDc2YTQiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTA3LTI1VDIxOjE0OjEwWiIsImV4cGlyYXRpb25EYXRlIjoiMjAyNC0wOC0yNFQyMToxNDoxMFoiLCJjcmVkZW50aWFsU2NoZW1hIjpbeyJ0eXBlIjoiSnNvblNjaGVtYSIsImlkIjoiaHR0cHM6Ly92Yy5zY2hlbWFzLmhvc3Qva2JjLnNjaGVtYS5qc29uIn1dfX0.VwvrU5Lmv3rn9rzXB0OCxe-MtE5R0876pXsXNLRuQjoqSNB5tBv_12NqrobwA-LkMzFwzdQ5-LWJni6grGdXCQ`
decoded, err := jwt.Decode(vcJwt)
assert.NoError(t, err)
assert.Equal(t, decoded.Header.ALG, "EdDSA")
assert.Equal(t, decoded.Header.KID, "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6InF4V1FKazE2RmhBekNBTlFKZGQyQ1dFWkpOempRb3FJdXZNRmJUZ1JMSEEifQ#0")
assert.NotZero(t, decoded.SignerDID)
}
func Test_Decode_Bad_Header(t *testing.T) {
vcJwt := `kakaHeader.eyJleHAiOjE3MjQ1MzQwNTAsImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbkY0VjFGS2F6RTJSbWhCZWtOQlRsRktaR1F5UTFkRldrcE9lbXBSYjNGSmRYWk5SbUpVWjFKTVNFRWlmUSIsImp0aSI6InVybjp2Yzp1dWlkOjlkMzdmMzY3LWE4ZDctNDY4Zi05NGYwLTk1NzAxNzBkNzZhNCIsIm5iZiI6MTcyMTk0MjA1MCwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW5GNFYxRkthekUyUm1oQmVrTkJUbEZLWkdReVExZEZXa3BPZW1wUmIzRkpkWFpOUm1KVVoxSk1TRUVpZlEiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpc3N1ZXIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW5GNFYxRkthekUyUm1oQmVrTkJUbEZLWkdReVExZEZXa3BPZW1wUmIzRkpkWFpOUm1KVVoxSk1TRUVpZlEifSwiaWQiOiJ1cm46dmM6dXVpZDo5ZDM3ZjM2Ny1hOGQ3LTQ2OGYtOTRmMC05NTcwMTcwZDc2YTQiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTA3LTI1VDIxOjE0OjEwWiIsImV4cGlyYXRpb25EYXRlIjoiMjAyNC0wOC0yNFQyMToxNDoxMFoiLCJjcmVkZW50aWFsU2NoZW1hIjpbeyJ0eXBlIjoiSnNvblNjaGVtYSIsImlkIjoiaHR0cHM6Ly92Yy5zY2hlbWFzLmhvc3Qva2JjLnNjaGVtYS5qc29uIn1dfX0.VwvrU5Lmv3rn9rzXB0OCxe-MtE5R0876pXsXNLRuQjoqSNB5tBv_12NqrobwA-LkMzFwzdQ5-LWJni6grGdXCQ`
_, err := jwt.Decode(vcJwt)
assert.Error(t, err)
}
func Test_Decode_Bad_Signature(t *testing.T) {
vcJwt := `eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbkY0VjFGS2F6RTJSbWhCZWtOQlRsRktaR1F5UTFkRldrcE9lbXBSYjNGSmRYWk5SbUpVWjFKTVNFRWlmUSMwIiwidHlwIjoiSldUIn0.eyJleHAiOjE3MjQ1MzQwNTAsImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbkY0VjFGS2F6RTJSbWhCZWtOQlRsRktaR1F5UTFkRldrcE9lbXBSYjNGSmRYWk5SbUpVWjFKTVNFRWlmUSIsImp0aSI6InVybjp2Yzp1dWlkOjlkMzdmMzY3LWE4ZDctNDY4Zi05NGYwLTk1NzAxNzBkNzZhNCIsIm5iZiI6MTcyMTk0MjA1MCwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW5GNFYxRkthekUyUm1oQmVrTkJUbEZLWkdReVExZEZXa3BPZW1wUmIzRkpkWFpOUm1KVVoxSk1TRUVpZlEiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpc3N1ZXIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW5GNFYxRkthekUyUm1oQmVrTkJUbEZLWkdReVExZEZXa3BPZW1wUmIzRkpkWFpOUm1KVVoxSk1TRUVpZlEifSwiaWQiOiJ1cm46dmM6dXVpZDo5ZDM3ZjM2Ny1hOGQ3LTQ2OGYtOTRmMC05NTcwMTcwZDc2YTQiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTA3LTI1VDIxOjE0OjEwWiIsImV4cGlyYXRpb25EYXRlIjoiMjAyNC0wOC0yNFQyMToxNDoxMFoiLCJjcmVkZW50aWFsU2NoZW1hIjpbeyJ0eXBlIjoiSnNvblNjaGVtYSIsImlkIjoiaHR0cHM6Ly92Yy5zY2hlbWFzLmhvc3Qva2JjLnNjaGVtYS5qc29uIn1dfX0.kakaSignature`
_, err := jwt.Decode(vcJwt)
assert.Error(t, err)
}
func Test_Decode_HeaderKID_InvalidDID(t *testing.T) {
vcJwt := `eyJhbGciOiJFZERTQSIsImtpZCI6Imtha2EiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE3MjQ1MzQwNTAsImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbkY0VjFGS2F6RTJSbWhCZWtOQlRsRktaR1F5UTFkRldrcE9lbXBSYjNGSmRYWk5SbUpVWjFKTVNFRWlmUSIsImp0aSI6InVybjp2Yzp1dWlkOjlkMzdmMzY3LWE4ZDctNDY4Zi05NGYwLTk1NzAxNzBkNzZhNCIsIm5iZiI6MTcyMTk0MjA1MCwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW5GNFYxRkthekUyUm1oQmVrTkJUbEZLWkdReVExZEZXa3BPZW1wUmIzRkpkWFpOUm1KVVoxSk1TRUVpZlEiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpc3N1ZXIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW5GNFYxRkthekUyUm1oQmVrTkJUbEZLWkdReVExZEZXa3BPZW1wUmIzRkpkWFpOUm1KVVoxSk1TRUVpZlEifSwiaWQiOiJ1cm46dmM6dXVpZDo5ZDM3ZjM2Ny1hOGQ3LTQ2OGYtOTRmMC05NTcwMTcwZDc2YTQiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTA3LTI1VDIxOjE0OjEwWiIsImV4cGlyYXRpb25EYXRlIjoiMjAyNC0wOC0yNFQyMToxNDoxMFoiLCJjcmVkZW50aWFsU2NoZW1hIjpbeyJ0eXBlIjoiSnNvblNjaGVtYSIsImlkIjoiaHR0cHM6Ly92Yy5zY2hlbWFzLmhvc3Qva2JjLnNjaGVtYS5qc29uIn1dfX0.VwvrU5Lmv3rn9rzXB0OCxe-MtE5R0876pXsXNLRuQjoqSNB5tBv_12NqrobwA-LkMzFwzdQ5-LWJni6grGdXCQ`
_, err := jwt.Decode(vcJwt)
assert.Error(t, err)
}

174
cli/pkg/web5/pexv2/pd.go Normal file
View File

@@ -0,0 +1,174 @@
package pexv2
import (
"encoding/base64"
"encoding/json"
"fmt"
"math/rand"
"olares-cli/pkg/web5/vc"
"strconv"
"github.com/PaesslerAG/jsonpath"
"github.com/santhosh-tekuri/jsonschema/v5"
)
// PresentationDefinition represents a DIF Presentation Definition defined [here].
// Presentation Definitions are objects that articulate what proofs a Verifier requires
//
// [here]: https://identity.foundation/presentation-exchange/#presentation-definition
type PresentationDefinition struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Purpose string `json:"purpose,omitempty"`
InputDescriptors []InputDescriptor `json:"input_descriptors"`
}
// InputDescriptor represents a DIF Input Descriptor defined [here].
// Input Descriptors are used to describe the information a Verifier requires of a Holder.
//
// [here]: https://identity.foundation/presentation-exchange/#input-descriptor
type InputDescriptor struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Purpose string `json:"purpose,omitempty"`
Constraints Constraints `json:"constraints"`
}
type tokenizedField struct {
name string
path string
}
// SelectCredentials selects vcJWTs based on the constraints defined in the input descriptor
func (ind InputDescriptor) SelectCredentials(vcJWTs []string) ([]string, error) {
jsonSchema := JSONSchema{
Schema: "http://json-schema.org/draft-07/schema#",
Type: "object",
Properties: make(map[string]Filter, len(ind.Constraints.Fields)),
Required: make([]string, 0, len(ind.Constraints.Fields)),
}
// Each Field can have multiple Paths. Add a 'tokenizedField' for each Path, and add the Filter to the JSON Schema
tokenizedFields := make([]tokenizedField, 0, len(ind.Constraints.Fields))
for _, field := range ind.Constraints.Fields {
name := strconv.FormatInt(rand.Int63(), 10) //nolint:gosec
for _, path := range field.Path {
tf := tokenizedField{name: name, path: path}
tokenizedFields = append(tokenizedFields, tf)
}
if field.Filter != nil {
jsonSchema.AddProperty(name, *field.Filter, true)
}
}
sch, err := json.Marshal(jsonSchema)
if err != nil {
return nil, fmt.Errorf("error marshalling schema: %w", err)
}
schema, err := jsonschema.CompileString(ind.ID, string(sch))
if err != nil {
return nil, fmt.Errorf("error compiling schema: %w", err)
}
matched := make([]string, 0, len(vcJWTs))
for _, vcJWT := range vcJWTs {
tokensFound := make(map[string]bool, len(tokenizedFields))
decoded, err := vc.Decode[vc.Claims](vcJWT)
if err != nil {
continue
}
var jwtPayload map[string]any
payload, err := base64.RawURLEncoding.DecodeString(decoded.JWT.Parts[1])
if err != nil {
continue
}
if err := json.Unmarshal(payload, &jwtPayload); err != nil {
continue
}
selectionCandidate := make(map[string]any)
for _, tf := range tokenizedFields {
if ok := tokensFound[tf.name]; ok {
continue
}
value, err := jsonpath.Get(tf.path, jwtPayload)
if err != nil {
continue
}
if value != nil {
selectionCandidate[tf.name] = value
tokensFound[tf.name] = true
}
}
if len(selectionCandidate) != len(ind.Constraints.Fields) {
continue
}
if err := schema.Validate(selectionCandidate); err != nil {
continue
}
matched = append(matched, vcJWT)
}
return matched, nil
}
// Constraints contains the requirements for a given Input Descriptor.
type Constraints struct {
Fields []Field `json:"fields,omitempty"`
}
// Field contains the requirements for a given field within a proof
type Field struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Path []string `json:"path,omitempty"`
Purpose string `json:"purpose,omitempty"`
Filter *Filter `json:"filter,omitempty"`
Optional bool `json:"optional,omitempty"`
Predicate *Optionality `json:"predicate,omitempty"`
}
// Optionality is a type alias for the possible values of the predicate field
type Optionality string
// Constants for Optionality values
const (
Required Optionality = "required"
Preferred Optionality = "preferred"
)
// Filter is a JSON Schema that is applied against the value of a field.
type Filter struct {
Type string `json:"type,omitempty"`
Pattern string `json:"pattern,omitempty"`
Const string `json:"const,omitempty"`
Contains *Filter `json:"contains,omitempty"`
}
// JSONSchema represents a minimal JSON Schema
type JSONSchema struct {
Schema string `json:"$schema"`
Type string `json:"type"`
Properties map[string]Filter `json:"properties"`
Required []string `json:"required"`
}
// AddProperty adds the provided Filter with the provided name to the JsonSchema
func (j *JSONSchema) AddProperty(name string, value Filter, required bool) {
j.Properties[name] = value
if required {
j.Required = append(j.Required, name)
}
}

View File

@@ -0,0 +1,32 @@
package pexv2
import "fmt"
// SelectCredentials selects vcJWTs based on the constraints defined in the presentation definition
func SelectCredentials(vcJWTs []string, pd PresentationDefinition) ([]string, error) {
matchSet := make(map[string]bool, len(vcJWTs))
matched := make([]string, 0, len(matchSet))
for _, inputDescriptor := range pd.InputDescriptors {
matches, err := inputDescriptor.SelectCredentials(vcJWTs)
if err != nil {
return nil, fmt.Errorf("failed to satisfy input descriptor constraints %s: %w", inputDescriptor.ID, err)
}
if len(matches) == 0 {
return matched, nil
}
// Add all matches to the match set
for _, vcJWT := range matches {
matchSet[vcJWT] = true
}
}
// add all unique matches to the matched slice
for k := range matchSet {
matched = append(matched, k)
}
return matched, nil
}

View File

@@ -0,0 +1,37 @@
package pexv2_test
import (
"fmt"
"testing"
"olares-cli/pkg/web5/pexv2"
"github.com/alecthomas/assert/v2"
"github.com/decentralized-identity/web5-go"
testify "github.com/stretchr/testify/assert"
)
type PresentationInput struct {
PresentationDefinition pexv2.PresentationDefinition `json:"presentationDefinition"`
CredentialJwts []string `json:"credentialJwts"`
}
type PresentationOutput struct {
SelectedCredentials []string `json:"selectedCredentials"`
}
func TestSelectCredentials(t *testing.T) {
testVectors, err := web5.LoadTestVectors[PresentationInput, PresentationOutput]("../web5-spec/test-vectors/presentation_exchange/select_credentials.json")
assert.NoError(t, err)
for _, vector := range testVectors.Vectors {
t.Run(vector.Description, func(t *testing.T) {
fmt.Println("Running test vector: ", vector.Description)
vcJwts, err := pexv2.SelectCredentials(vector.Input.CredentialJwts, vector.Input.PresentationDefinition)
assert.NoError(t, err)
testify.ElementsMatch(t, vector.Output.SelectedCredentials, vcJwts)
})
}
}

View File

@@ -0,0 +1,150 @@
package vc_test
import (
"encoding/json"
"fmt"
"time"
"olares-cli/pkg/web5/dids/didkey"
"olares-cli/pkg/web5/vc"
)
// Demonstrates how to create, sign, and verify a Verifiable Credential using the vc package.
func Example() {
// create sample issuer and subject DIDs
issuer, err := didkey.Create()
if err != nil {
panic(err)
}
subject, err := didkey.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"])
// Output: Randy McRando
}
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
}
// Demonstrates how to use a strongly typed credential subject
func Example_stronglyTyped() {
issuer, err := didkey.Create()
if err != nil {
panic(err)
}
subject, err := didkey.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)
// Output: Randy McRando
}
// Demonstrates how to use a mix of strongly typed and untyped credential subjects with the vc package.
func Example_mixed() {
issuer, err := didkey.Create()
if err != nil {
panic(err)
}
subject, err := didkey.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"])
// Output: Randy McRando
}
// Demonstrates how to create a Verifiable Credential
func ExampleCreate() {
claims := vc.Claims{"name": "Randy McRando"}
cred := vc.Create(claims)
bytes, err := json.MarshalIndent(cred, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(bytes))
}
// Demonstrates how to create a Verifiable Credential with options
func ExampleCreate_options() {
claims := vc.Claims{"id": "1234"}
issuanceDate := time.Now().UTC().Add(10 * time.Hour)
expirationDate := issuanceDate.Add(30 * time.Hour)
cred := vc.Create(
claims,
vc.ID("hehecustomid"),
vc.Contexts("https://nocontextisbestcontext.gov"),
vc.Types("StreetCredential"),
vc.IssuanceDate(issuanceDate),
vc.ExpirationDate(expirationDate),
)
bytes, err := json.MarshalIndent(cred, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(bytes))
}

246
cli/pkg/web5/vc/vc.go Normal file
View File

@@ -0,0 +1,246 @@
package vc
import (
"fmt"
"time"
"olares-cli/pkg/web5/dids/did"
"olares-cli/pkg/web5/jwt"
"github.com/google/uuid"
)
// these constants are defined in the W3C Verifiable Credential Data Model specification for:
// - [Context]
// - [Type]
//
// [Context]: https://www.w3.org/TR/vc-data-model/#contexts
// [Type]: https://www.w3.org/TR/vc-data-model/#dfn-type
const (
BaseContext = "https://www.w3.org/2018/credentials/v1"
BaseType = "VerifiableCredential"
)
// DataModel represents the W3C Verifiable Credential Data Model defined [here]
//
// [here]: https://www.w3.org/TR/vc-data-model/
type DataModel[T CredentialSubject] struct {
Context []string `json:"@context"` // https://www.w3.org/TR/vc-data-model/#contexts
Type []string `json:"type"` // https://www.w3.org/TR/vc-data-model/#dfn-type
Issuer string `json:"issuer"` // https://www.w3.org/TR/vc-data-model/#issuer
CredentialSubject T `json:"credentialSubject"` // https://www.w3.org/TR/vc-data-model/#credential-subject
ID string `json:"id,omitempty"` // https://www.w3.org/TR/vc-data-model/#identifiers
IssuanceDate string `json:"issuanceDate"` // https://www.w3.org/TR/vc-data-model/#issuance-date
ExpirationDate string `json:"expirationDate,omitempty"` // https://www.w3.org/TR/vc-data-model/#expiration
CredentialSchema []CredentialSchema `json:"credentialSchema,omitempty"` // https://www.w3.org/TR/vc-data-model-2.0/#data-schemas
Evidence []Evidence `json:"evidence,omitempty"` // https://www.w3.org/TR/vc-data-model/#evidence
}
// Evidence represents the evidence property of a Verifiable Credential.
type Evidence struct {
ID string `json:"id,omitempty"`
Type string `json:"type,omitempty"`
// todo is `AdditionalFields` the right name?
AdditionalFields map[string]interface{}
}
// CredentialSubject is implemented by any type that can be used as the CredentialSubject
// of a Verifiable Credential.
//
// # Note
//
// The VC Data Model specification states that id is not a required field for [CredentialSubject]. However,
// we've chosen to require it in order to necessitate that all credential's include a subject as we were unable
// to find a use case where a credential would not be issued to a single subject. Further, the spec states that
// [vc-jwt] requires the sub be set to the id of the CredentialSubject which becomes difficult to assert while
// also providing the ability to leverage strongly typed claims.
//
// [CredentialSubject]: https://www.w3.org/TR/vc-data-model/#credential-subject
// [vc-jwt]: https://www.w3.org/TR/vc-data-model/#json-web-token
type CredentialSubject interface {
GetID() string
SetID(id string)
}
// CredentialSchema represents the credentialSchema property of a Verifiable Credential.
// more information can be found [here]
//
// [here]: https://www.w3.org/TR/vc-data-model-2.0/#data-schemas
type CredentialSchema struct {
Type string `json:"type"`
ID string `json:"id"`
}
// Claims is a type alias for a map[string]any that can be used to represent the claims of a Verifiable Credential
// when the structure of the claims is not known at compile time.
type Claims map[string]any
// GetID returns the id of the CredentialSubject. used to set the sub claim of a vc-jwt in [vcjwt.Sign]
func (c Claims) GetID() string {
id, _ := c["id"].(string)
return id
}
// SetID sets the id of the CredentialSubject. used to set the sub claim of a vc-jwt in [vcjwt.Verify]
func (c Claims) SetID(id string) {
c["id"] = id
}
// createOptions contains all of the options that can be passed to [Create]
type createOptions struct {
contexts []string
types []string
id string
issuanceDate time.Time
expirationDate time.Time
schemas []CredentialSchema
evidence []Evidence
}
// CreateOption is the return type of all Option functions that can be passed to [Create]
type CreateOption func(*createOptions)
// Contexts can be used to add additional contexts to the Verifiable Credential created by [Create]
func Contexts(contexts ...string) CreateOption {
return func(o *createOptions) {
if o.contexts != nil {
o.contexts = append(o.contexts, contexts...)
} else {
o.contexts = contexts
}
}
}
// Schemas can be used to include JSON Schemas within the Verifiable Credential created by [Create]
// more information can be found [here]
//
// [here]: https://www.w3.org/TR/vc-data-model-2.0/#data-schemas
func Schemas(schemas ...string) CreateOption {
return func(o *createOptions) {
if o.schemas != nil {
o.schemas = make([]CredentialSchema, 0, len(schemas))
}
for _, schema := range schemas {
o.schemas = append(o.schemas, CredentialSchema{Type: "JsonSchema", ID: schema})
}
}
}
// Types can be used to add additional types to the Verifiable Credential created by [Create]
func Types(types ...string) CreateOption {
return func(o *createOptions) {
if o.types != nil {
o.types = append(o.types, types...)
} else {
o.types = types
}
}
}
// ID can be used to override the default ID generated by [Create]
func ID(id string) CreateOption {
return func(o *createOptions) {
o.id = id
}
}
// IssuanceDate can be used to override the default issuance date generated by [Create]
func IssuanceDate(issuanceDate time.Time) CreateOption {
return func(o *createOptions) {
o.issuanceDate = issuanceDate
}
}
// ExpirationDate can be used to set the expiration date of the Verifiable Credential created by [Create]
func ExpirationDate(expirationDate time.Time) CreateOption {
return func(o *createOptions) {
o.expirationDate = expirationDate
}
}
// Evidences can be used to set the evidence array of the Verifiable Credential created by [Create]
func Evidences(evidence ...Evidence) CreateOption {
return func(o *createOptions) {
o.evidence = evidence
}
}
// Create returns a new Verifiable Credential with the provided claims and options.
// if no options are provided, the following defaults will be used:
// - ID: urn:vc:uuid:<uuid>
// - Contexts: ["https://www.w3.org/2018/credentials/v1"]
// - Types: ["VerifiableCredential"]
// - IssuanceDate: time.Now()
//
// # Note
//
// Any additional contexts or types provided will be appended to the defaults in order to remain conformant with
// the W3C Verifiable Credential Data Model specification
func Create[T CredentialSubject](claims T, opts ...CreateOption) DataModel[T] {
o := createOptions{
id: "urn:vc:uuid:" + uuid.New().String(),
contexts: []string{BaseContext},
types: []string{BaseType},
issuanceDate: time.Now(),
}
for _, f := range opts {
f(&o)
}
cred := DataModel[T]{
Context: o.contexts,
Type: o.types,
ID: o.id,
IssuanceDate: o.issuanceDate.UTC().Format(time.RFC3339),
CredentialSubject: claims,
Evidence: o.evidence,
}
if len(o.schemas) > 0 {
cred.CredentialSchema = o.schemas
}
if (o.expirationDate != time.Time{}) {
cred.ExpirationDate = o.expirationDate.UTC().Format(time.RFC3339)
}
return cred
}
// Sign returns a signed JWT conformant with the [vc-jwt] format. sets the provided vc as value of
// the "vc" claim in the jwt. It returns the signed jwt and an error if the signing fails.
//
// [vc-jwt]: https://www.w3.org/TR/vc-data-model/#json-web-token
func (vc DataModel[T]) Sign(bearerDID did.BearerDID, opts ...jwt.SignOpt) (string, error) {
vc.Issuer = bearerDID.URI
jwtClaims := jwt.Claims{
Issuer: vc.Issuer,
JTI: vc.ID,
Subject: vc.CredentialSubject.GetID(),
}
t, err := time.Parse(time.RFC3339, vc.IssuanceDate)
if err != nil {
return "", fmt.Errorf("failed to parse issuance date: %w", err)
}
jwtClaims.NotBefore = t.Unix()
if vc.ExpirationDate != "" {
t, err := time.Parse(time.RFC3339, vc.ExpirationDate)
if err != nil {
return "", fmt.Errorf("failed to parse expiration date: %w", err)
}
jwtClaims.Expiration = t.Unix()
}
jwtClaims.Misc = make(map[string]any)
jwtClaims.Misc["vc"] = vc
// typ must be set to "JWT" as per the spec
opts = append(opts, jwt.Type("JWT"))
return jwt.Sign(jwtClaims, bearerDID, opts...)
}

View File

@@ -0,0 +1,98 @@
package vc_test
import (
"slices"
"testing"
"time"
"olares-cli/pkg/web5/dids/didkey"
"olares-cli/pkg/web5/vc"
"github.com/alecthomas/assert/v2"
)
func TestCreate_Defaults(t *testing.T) {
cred := vc.Create(vc.Claims{"id": "1234"})
assert.Equal(t, 1, len(cred.Context))
assert.Equal(t, vc.BaseContext, cred.Context[0])
assert.Equal(t, 1, len(cred.Type))
assert.Equal(t, vc.BaseType, cred.Type[0])
assert.Contains(t, cred.ID, "urn:vc:uuid:")
assert.NotZero(t, cred.IssuanceDate)
_, err := time.Parse(time.RFC3339, cred.IssuanceDate)
assert.NoError(t, err)
assert.Equal(t, "1234", cred.CredentialSubject["id"])
}
func TestCreate_Options(t *testing.T) {
claims := vc.Claims{"id": "1234"}
issuanceDate := time.Now().UTC().Add(10 * time.Hour)
expirationDate := issuanceDate.Add(30 * time.Hour)
cred := vc.Create(
claims,
vc.ID("hehecustomid"),
vc.Contexts("https://nocontextisbestcontext.gov"),
vc.Types("StreetCredential"),
vc.IssuanceDate(issuanceDate),
vc.ExpirationDate(expirationDate),
vc.Schemas("https://example.org/examples/degree.json"),
vc.Evidences(vc.Evidence{
ID: "evidenceID",
Type: "Insufficient",
AdditionalFields: map[string]interface{}{
"kind": "circumstantial",
"checks": []string{"motive", "cell_tower_logs"},
},
}),
)
assert.Equal(t, 2, len(cred.Context))
assert.True(t, slices.Contains(cred.Context, "https://nocontextisbestcontext.gov"))
assert.True(t, slices.Contains(cred.Context, vc.BaseContext))
assert.Equal(t, 2, len(cred.Type))
assert.True(t, slices.Contains(cred.Type, "StreetCredential"))
assert.True(t, slices.Contains(cred.Type, vc.BaseType))
assert.Equal(t, "hehecustomid", cred.ID)
assert.NotZero(t, cred.ExpirationDate)
assert.Equal(t, 1, len(cred.CredentialSchema))
assert.Equal(t, "https://example.org/examples/degree.json", cred.CredentialSchema[0].ID)
assert.Equal(t, "JsonSchema", cred.CredentialSchema[0].Type)
assert.Equal(t, 1, len(cred.Evidence))
assert.Equal(t, "evidenceID", cred.Evidence[0].ID)
assert.Equal(t, "Insufficient", cred.Evidence[0].Type)
assert.Equal(t, "circumstantial", cred.Evidence[0].AdditionalFields["kind"])
assert.Equal(t, []string{"motive", "cell_tower_logs"}, cred.Evidence[0].AdditionalFields["checks"].([]string)) // nolint:forcetypeassert
}
func TestSign(t *testing.T) {
issuer, err := didkey.Create()
assert.NoError(t, err)
subject, err := didkey.Create()
assert.NoError(t, err)
claims := vc.Claims{"id": subject.URI, "name": "Randy McRando"}
cred := vc.Create(claims)
vcJWT, err := cred.Sign(issuer)
assert.NoError(t, err)
assert.NotZero(t, vcJWT)
// TODO: make test more reliable by not depending on another function in this package (Moe - 2024-02-25)
decoded, err := vc.Verify[vc.Claims](vcJWT)
assert.NoError(t, err)
assert.NotZero(t, decoded)
}

157
cli/pkg/web5/vc/vcjwt.go Normal file
View File

@@ -0,0 +1,157 @@
package vc
import (
"encoding/json"
"errors"
"fmt"
"slices"
"time"
"olares-cli/pkg/web5/jwt"
)
// Verify decodes and verifies the vc-jwt. It checks for the presence of required fields and verifies the jwt.
// It returns the decoded vc-jwt and the verification result.
func Verify[T CredentialSubject](vcJWT string) (DecodedVCJWT[T], error) {
decoded, err := Decode[T](vcJWT)
if err != nil {
return decoded, err
}
return decoded, decoded.Verify()
}
// Decode decodes a vc-jwt as per the [spec] and returns [DecodedVCJWT].
//
// # Note
//
// This function uses certain fields from the jwt claims to eagrly populate the vc model as described
// in the encoding section of the spec. The jwt fields will clobber any values that exist in the vc model.
// While the jwt claims should match the counterpart values in the vc model, it's possible that they don't
// but there would be no way to know if they don't match given that they're overwritten.
//
// [spec]: https://www.w3.org/TR/vc-data-model/#json-web-token
func Decode[T CredentialSubject](vcJWT string) (DecodedVCJWT[T], error) {
decoded, err := jwt.Decode(vcJWT)
if err != nil {
return DecodedVCJWT[T]{}, fmt.Errorf("failed to decode vc-jwt: %w", err)
}
if decoded.Claims.Misc == nil {
return DecodedVCJWT[T]{}, errors.New("vc-jwt missing vc claim")
}
if _, ok := decoded.Claims.Misc["vc"]; ok == false {
return DecodedVCJWT[T]{}, errors.New("vc-jwt missing vc claim")
}
bytes, err := json.Marshal(decoded.Claims.Misc["vc"])
if err != nil {
return DecodedVCJWT[T]{}, fmt.Errorf("failed to decode vc claim: %w", err)
}
var vc DataModel[T]
if err := json.Unmarshal(bytes, &vc); err != nil {
return DecodedVCJWT[T]{}, fmt.Errorf("failed to decode vc claim: %w", err)
}
if vc.Type == nil {
return DecodedVCJWT[T]{}, errors.New("vc-jwt missing vc type")
}
// the following conditionals are included to conform with the jwt decoding section
// of the specification defined here: https://www.w3.org/TR/vc-data-model/#jwt-decoding
if decoded.Claims.Issuer != "" {
vc.Issuer = decoded.Claims.Issuer
}
if decoded.Claims.JTI != "" {
vc.ID = decoded.Claims.JTI
}
if decoded.Claims.Subject != "" {
vc.CredentialSubject.SetID(decoded.Claims.Subject)
}
if decoded.Claims.Expiration != 0 {
vc.ExpirationDate = time.Unix(decoded.Claims.Expiration, 0).UTC().Format(time.RFC3339)
}
if decoded.Claims.NotBefore != 0 {
vc.IssuanceDate = time.Unix(decoded.Claims.NotBefore, 0).UTC().Format(time.RFC3339)
}
return DecodedVCJWT[T]{
JWT: decoded,
VC: vc,
}, nil
}
// DecodedVCJWT represents a decoded vc-jwt. It contains the decoded jwt and decoded vc data model
type DecodedVCJWT[T CredentialSubject] struct {
JWT jwt.Decoded
VC DataModel[T]
}
// Verify verifies the decoded vc-jwt. It checks for the presence of required fields and verifies the jwt.
func (vcjwt DecodedVCJWT[T]) Verify() error {
if vcjwt.JWT.Header.TYP != "JWT" {
return errors.New("invalid typ")
}
if vcjwt.VC.Issuer == "" {
return errors.New("missing issuer")
}
if vcjwt.VC.ID == "" {
return errors.New("missing id")
}
if vcjwt.VC.IssuanceDate == "" {
return errors.New("missing issuance date")
}
issuanceDate, err := time.Parse(time.RFC3339, vcjwt.VC.IssuanceDate)
if err != nil {
return fmt.Errorf("failed to parse issuance date: %w", err)
}
if time.Now().UTC().Before(issuanceDate.UTC()) {
return fmt.Errorf("vc cannot be used before %s", vcjwt.VC.IssuanceDate)
}
if vcjwt.VC.ExpirationDate != "" {
exp, err := time.Parse(time.RFC3339, vcjwt.VC.ExpirationDate)
if err != nil {
return fmt.Errorf("failed to parse expiration date: %w", err)
}
if time.Now().UTC().After(exp.UTC()) {
return fmt.Errorf("vc expired on %s", vcjwt.VC.ExpirationDate)
}
}
if vcjwt.VC.Type == nil || len(vcjwt.VC.Type) == 0 {
return errors.New("missing type")
}
if slices.Contains(vcjwt.VC.Type, BaseType) == false {
return fmt.Errorf("missing base type: %s", BaseType)
}
if vcjwt.VC.Context == nil || len(vcjwt.VC.Context) == 0 {
return errors.New("missing @context")
}
if slices.Contains(vcjwt.VC.Context, BaseContext) == false {
return fmt.Errorf("missing base @context: %s", BaseContext)
}
err = vcjwt.JWT.Verify()
if err != nil {
return fmt.Errorf("integrity check mismatch: %w", err)
}
return nil
}

View File

@@ -0,0 +1,212 @@
package vc_test
import (
"fmt"
"testing"
"time"
"olares-cli/pkg/web5/dids/didkey"
"olares-cli/pkg/web5/jwt"
"olares-cli/pkg/web5/vc"
"github.com/alecthomas/assert/v2"
"github.com/decentralized-identity/web5-go"
)
type vector struct {
description string
input string
errors bool
}
func TestDecode(t *testing.T) {
// TODO: move these to web5-spec repo test-vectors (Moe - 2024-02-24)
vectors := []vector{
{
description: "fail to decode jwt",
input: "doodoo",
errors: true,
},
{
description: "no claims",
input: "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJa3RPTVZGRU5ETkhZVkpJTm1OeWJpMTFTWFk0ZDBoT1pqZHlSMlkyVUY5RFMxZzJkbmsyTUdjMWQyc2lmUSMwIn0.e30.1iq9_pDtMlzL22h6xVY77nRNfXnR3oFU2kNYDAM52dPAs0l8zLL6AJ18B8rz9HziYzRo4Zo_jyYhq4nlHE3lBw",
errors: true,
}, {
description: "no vc claim",
input: "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJa0YyZFMxVVNsTlRWakZKT0dob1NrUmFlWGw1YUhnMmVHSlZjamRQT0RWMWRFMWpaa3RLVDJOblFWVWlmUSMwIn0.eyJoZWhlIjoiaGkifQ.QQ5aottVrsHRisxx7vRzin9CnyOcxeScxLOIy5qI30pV2FkXXBe3BdyujLS7i7M0CHW0eS9XhaVKe76504RZCQ",
errors: true,
}, {
description: "vc claim wrong type",
input: "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbEZvUkhsdFprdzFaQzB0WjFGQ1prOVNOMlZFYjBrelprTjJOVUV0Y3pBMGFYZHlZMGRsTkVWd1lsa2lmUSMwIn0.eyJ2YyI6ImhpIn0.O_-xPUAZhi9W3OD1pJn4wN5Q9nZKYXcmtJPhWuk6WxlOXMca2jNXyjYpEKCJ1vFWZ4OHfSifErPvClLsH8-MCQ",
errors: true,
}, {
description: "legit",
input: "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbVJwVGkxRlEydGhaRTVEVlVKZlUxRkhTVFJtVUdOZlluVmZObmt3VWpKRFdEUllkMjlTUzBjNFVEZ2lmUSMwIn0.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW1ScFRpMUZRMnRoWkU1RFZVSmZVMUZIU1RSbVVHTmZZblZmTm5rd1VqSkRXRFJZZDI5U1MwYzRVRGdpZlEiLCJqdGkiOiJ1cm46dmM6dXVpZDoxOGQ5OTZjZi03N2YwLTRkYjgtOGQ5MS0zNGI1ZDY1NzcwNmUiLCJuYmYiOjE3MDg3NTY3ODUsInN1YiI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbVJwVGkxRlEydGhaRTVEVlVKZlUxRkhTVFJtVUdOZlluVmZObmt3VWpKRFdEUllkMjlTUzBjNFVEZ2lmUSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpZUNJNkltUnBUaTFGUTJ0aFpFNURWVUpmVTFGSFNUUm1VR05mWW5WZk5ua3dVakpEV0RSWWQyOVNTMGM0VURnaWZRIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW1ScFRpMUZRMnRoWkU1RFZVSmZVMUZIU1RSbVVHTmZZblZmTm5rd1VqSkRXRFJZZDI5U1MwYzRVRGdpZlEifSwiaWQiOiJ1cm46dmM6dXVpZDoxOGQ5OTZjZi03N2YwLTRkYjgtOGQ5MS0zNGI1ZDY1NzcwNmUiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTAyLTI0VDA2OjM5OjQ1WiJ9fQ.Y1-9dFop7bg_0jvgZMLyE3CPjnSXH9SGTHeA_jn5HosYbhST8y_pK7LcDeCYLSgDfiIOeVsvJFqOr3XT2J2cDA",
errors: false,
},
}
for _, tt := range vectors {
t.Run(tt.description, func(t *testing.T) {
decoded, err := vc.Decode[vc.Claims](tt.input)
if tt.errors == true {
assert.Error(t, err)
assert.Equal(t, vc.DecodedVCJWT[vc.Claims]{}, decoded)
} else {
assert.NoError(t, err)
assert.NotEqual(t, vc.DecodedVCJWT[vc.Claims]{}, decoded)
}
})
}
}
func TestDecode_SetClaims(t *testing.T) {
issuer, err := didkey.Create()
assert.NoError(t, err)
subject, err := didkey.Create()
assert.NoError(t, err)
subjectClaims := vc.Claims{
"firstName": "Randy",
"lastName": "McRando",
}
issuanceDate := time.Now().UTC()
// missing issuer
jwtClaims := jwt.Claims{
JTI: "abcd123",
Issuer: issuer.URI,
Subject: subject.URI,
NotBefore: issuanceDate.Unix(),
Expiration: issuanceDate.Add(time.Hour).Unix(),
Misc: map[string]any{
"vc": vc.DataModel[vc.Claims]{
CredentialSubject: subjectClaims,
Type: []string{"Something"},
},
},
}
vcJWT, err := jwt.Sign(jwtClaims, issuer)
assert.NoError(t, err)
decoded, err := vc.Decode[vc.Claims](vcJWT)
assert.NoError(t, err)
assert.Equal(t, jwtClaims.JTI, decoded.VC.ID)
assert.Equal(t, jwtClaims.Issuer, decoded.VC.Issuer)
assert.Equal(t, jwtClaims.Subject, decoded.VC.CredentialSubject.GetID())
assert.Equal(t, issuanceDate.Format(time.RFC3339), decoded.VC.IssuanceDate)
assert.NotZero(t, decoded.VC.ExpirationDate)
}
func TestVerify(t *testing.T) {
vectors := []vector{
{
description: "no typ header",
input: "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJa1JXZDFwWmFWY3lTalZETkc1cmQyRkdRV0pKVUY5MlIzTnJTamhKT1VKRk5IcE9RVGgxUkdZMVZsVWlmUSMwIn0.eyJleHAiOjI2NTUyMTM3MDQsImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJa1JXZDFwWmFWY3lTalZETkc1cmQyRkdRV0pKVUY5MlIzTnJTamhKT1VKRk5IcE9RVGgxUkdZMVZsVWlmUSIsImp0aSI6ImFiY2QxMjMiLCJuYmYiOjE3MDkxMzM3MDQsInN1YiI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbWRpZVhwcGNuTmthekpOVW14WVV5MWtURkU0TkZWbVRrRlZjbUp5T0hZd2FESkViblZVTUdSV1kxRWlmUSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiZmlyc3ROYW1lIjoiUmFuZHkiLCJsYXN0TmFtZSI6Ik1jUmFuZG8ifSwiaXNzdWFuY2VEYXRlIjoiIn19.7trsEIJxKlQhvCH3F-w4ZTessbGaCG6X_6di8sl3qTRdEk8QFyv7xvFSFXBcX4XC6i_DfWndlhj1cdEtL9B1CA",
errors: true,
}, {
description: "invalid typ header",
input: "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbmxXTFhvMGNqZGlNMmRtZEV0U016ZEpNR3N3VVhsSk0yZHBTa0Z3ZFhsU1FtMXpZVXBSWjI0eWVUUWlmUSMwIiwidHlwIjoiS2FrYW1pbWkifQ.eyJleHAiOjI2NTUyMTM3ODMsImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbmxXTFhvMGNqZGlNMmRtZEV0U016ZEpNR3N3VVhsSk0yZHBTa0Z3ZFhsU1FtMXpZVXBSWjI0eWVUUWlmUSIsImp0aSI6ImFiY2QxMjMiLCJuYmYiOjE3MDkxMzM3ODMsInN1YiI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbmRuTTFGUVJsSmplUzAxVXpaNlNqZEVWMmx4U0Vwd1RHTlJaRmhVVWsxWk1td3hhRTEyWm1reFVXTWlmUSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiZmlyc3ROYW1lIjoiUmFuZHkiLCJsYXN0TmFtZSI6Ik1jUmFuZG8ifSwiaXNzdWFuY2VEYXRlIjoiIn19.fE58Vtqg5-oOQKvRCiJHCspZaqmGOtEIlUTf8TqWpviWGndpZWj1XofcUfcNFLWTHnk6H-2ku9FA7x_t4ymgAA",
errors: true,
}, {
description: "no id",
input: "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJakl5T1RoZldFSkhPRk41Y1drNFJuQnlWekF4U0RGblpHdzRlRXRZVERaUlNsaDJhR05mWW5sek1EZ2lmUSMwIiwidHlwIjoiSldUIn0.eyJleHAiOjM2MDEyOTMyODIsImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJakl5T1RoZldFSkhPRk41Y1drNFJuQnlWekF4U0RGblpHdzRlRXRZVERaUlNsaDJhR05mWW5sek1EZ2lmUSIsIm5iZiI6MTcwOTEzMzI4Miwic3ViIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpZUNJNkluUXdhMmRYUldaSU5scG5TVGxGVW5wMFIxazBMV3RzU2paUFlVVTFTSGhrWlZOUFRUUmtPRVE0WW1NaWZRIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiIiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJmaXJzdE5hbWUiOiJSYW5keSIsImxhc3ROYW1lIjoiTWNSYW5kbyJ9LCJpc3N1YW5jZURhdGUiOiIifX0.e7ITj0XXqXBqulsz5Bv0ACtBY9T_EW2jdJbM1Cv5C1Rg1uy_fVWN0asYOOgT75oU87W8dV8bKM_2YMns1JoOCA",
errors: true,
}, {
description: "no issuer",
input: "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJblYzUTJkV2IxVmplbTFpV1ZoWFVFbGFOMDgyTkdRMkxYTXhkamhPYkRacVoyUnpSMEpOV1d0cVV6QWlmUSMwIiwidHlwIjoiSldUIn0.eyJleHAiOjM2MDEyOTMyOTksImp0aSI6ImFiY2QxMjMiLCJuYmYiOjE3MDkxMzMyOTksInN1YiI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJazVQUkRKQ01qSnVOaTFZTkdOTVNHc3hha2hEWDFaNFptVkNWVWd3UVdoYVJrcE1XRFJ1TTNwWFRWVWlmUSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiZmlyc3ROYW1lIjoiUmFuZHkiLCJsYXN0TmFtZSI6Ik1jUmFuZG8ifSwiaXNzdWFuY2VEYXRlIjoiIn19.DZs9KgQLD8k0ouL1W8ENBzWXOKSJZ_plm7UOmA2VQtmyqUISnB_KxqncY-MIWzTXfPObQzMALY-ZVjYgqABRDA",
errors: true,
}, {
description: "no issuance date",
input: "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJa0pNYVhaT1Iza3hSV0Z5VDJGZlUzUkVPSEY1TjFaT1F6TnhhRW90TFZSdlJYTjVMVXBCYm5WdlJtOGlmUSMwIiwidHlwIjoiSldUIn0.eyJleHAiOjM2MDEyOTMzMTksImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJa0pNYVhaT1Iza3hSV0Z5VDJGZlUzUkVPSEY1TjFaT1F6TnhhRW90TFZSdlJYTjVMVXBCYm5WdlJtOGlmUSIsImp0aSI6ImFiY2QxMjMiLCJzdWIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SWsxM1VtVndiWFo2T1hwaVVsSm5Za1l4UVc1cFluSnlhelZpVlV4NlUxWkNibEl0YWxGM2JYQlRVazBpZlEiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImlzc3VlciI6IiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImZpcnN0TmFtZSI6IlJhbmR5IiwibGFzdE5hbWUiOiJNY1JhbmRvIn0sImlzc3VhbmNlRGF0ZSI6IiJ9fQ.1O8nUSZIUrbfqp0ulLabhyME7b7nQSx9lPwkkLvNmJGHaNCgF3EodDM88V8Zzke1meDWRM2tAUeZrTDCUWPGBQ",
errors: true,
}, {
description: "issuance date in future",
input: "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbFZUV0dSNlNVSkdNekZ0YWtkVWNrSTBPWGRNZWt4MGMyNWlibmxrTlVwbGVHOTBkR1prV0RkNVlqQWlmUSMwIiwidHlwIjoiSldUIn0.eyJleHAiOjQ1NDczNzMzNDQsImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbFZUV0dSNlNVSkdNekZ0YWtkVWNrSTBPWGRNZWt4MGMyNWlibmxrTlVwbGVHOTBkR1prV0RkNVlqQWlmUSIsImp0aSI6ImFiY2QxMjMiLCJuYmYiOjI2NTUyMTMzNDQsInN1YiI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbnBIYzJFMk5rTTVWRXh1TmxscE1FdE5RbnBST1ZOQ1gyMVBZa1Y0UTJrdGVHaEdaWGRzVDBGQkxUZ2lmUSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiZmlyc3ROYW1lIjoiUmFuZHkiLCJsYXN0TmFtZSI6Ik1jUmFuZG8ifSwiaXNzdWFuY2VEYXRlIjoiIn19.0VxukH5AOhQ1WGzInDFXkOgr5gzvGCKLhFkbSsD76GcKqcDsuek1uQz-IoTOoZIA97d1yczFJCLMNMRV05ARCA",
errors: true,
}, {
description: "no context",
input: "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJalYyUnpGUlEyOHlTR3BNTjFCWlJraERNM2t3ZDE5VGQzTkZRV2xCYldGU1lXRkZWRmM1V2sxRk56QWlmUSMwIiwidHlwIjoiSldUIn0.eyJleHAiOjM2MDEyOTMzNzQsImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJalYyUnpGUlEyOHlTR3BNTjFCWlJraERNM2t3ZDE5VGQzTkZRV2xCYldGU1lXRkZWRmM1V2sxRk56QWlmUSIsImp0aSI6ImFiY2QxMjMiLCJuYmYiOjE3MDkxMzMzNzQsInN1YiI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbU5HVEhwSmMwOUVWME0wTVRKVGJuWnpSSFJuVkVSWGJuTlplSGgyT0dNMGFEZHVTRE5YZUZoVFprRWlmUSIsInZjIjp7IkBjb250ZXh0IjpudWxsLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImlzc3VlciI6IiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImZpcnN0TmFtZSI6IlJhbmR5IiwibGFzdE5hbWUiOiJNY1JhbmRvIn0sImlzc3VhbmNlRGF0ZSI6IiJ9fQ.pixi8ODIn1TpUwTiQ_GJhP8vLgr8XNX1CBuXhtk9VnU4DJ137zHX30Z_BVgW7oFlrMcDpzq67A3PHaw6JHgdDA",
errors: true,
}, {
description: "missing base context",
input: "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbDlpY3pZMGN6SnBkMkp4ZFc5RGNrRmxSbkE0WHprMk1UaHdjblZ3WVdGMldtWmtjMDFpZW1oaFVuTWlmUSMwIiwidHlwIjoiSldUIn0.eyJleHAiOjM2MDEyOTM0MDAsImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbDlpY3pZMGN6SnBkMkp4ZFc5RGNrRmxSbkE0WHprMk1UaHdjblZ3WVdGMldtWmtjMDFpZW1oaFVuTWlmUSIsImp0aSI6ImFiY2QxMjMiLCJuYmYiOjE3MDkxMzM0MDAsInN1YiI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbWh2Tlc5SE1YazBjVFpQWVcxWVlWaEpUV3hVVlhScVYzRXhPRkF0TlcxRk5HNTRPVk5EVkVsMlZXOGlmUSIsInZjIjp7IkBjb250ZXh0IjpbIlN0cmVldENyZWRlbnRpYWwiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiIiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJmaXJzdE5hbWUiOiJSYW5keSIsImxhc3ROYW1lIjoiTWNSYW5kbyJ9LCJpc3N1YW5jZURhdGUiOiIifX0.opzudBm-S5dyAWrKrNMF0XR0Ol_98OeH7Zvt-xaYlLQPRrGzKsFFTsr_crfQHQaX27WPTKacLBDc4C2ocB-1Aw",
errors: true,
}, {
description: "no type",
input: "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbTVUVHpOalMydFJkelpCWWs5dGEyazNZa1ZPYVZGeVRXc3lkVE0xT0hSSGQxcHFaRFpSTm5CeVUyOGlmUSMwIiwidHlwIjoiSldUIn0.eyJleHAiOjM2MDEyOTM0MzEsImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbTVUVHpOalMydFJkelpCWWs5dGEyazNZa1ZPYVZGeVRXc3lkVE0xT0hSSGQxcHFaRFpSTm5CeVUyOGlmUSIsImp0aSI6ImFiY2QxMjMiLCJuYmYiOjE3MDkxMzM0MzEsInN1YiI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJa1JIUm1KRWVVVXpZalJHYUZwNVpXNWtlV3h2TTBwbWRsUnVaMkZWV0Y5b1ltWm9lR2szV1RSNmJYY2lmUSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOm51bGwsImlzc3VlciI6IiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImZpcnN0TmFtZSI6IlJhbmR5IiwibGFzdE5hbWUiOiJNY1JhbmRvIn0sImlzc3VhbmNlRGF0ZSI6IiJ9fQ.yrvOZc58oFqEXpMs6rk4E0QDLv28gjjunNFSafx0yV6tmn0nYO2btJnawPusrTcHt0tTjxB5SMUEyo6m7kWsAw",
errors: true,
}, {
description: "missing base type",
input: "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbHBOYmt0aGRYZERkbWx3YUd4V1NFVXRjVlZ0T1VveVQyWk5RWE5JVkc5cFNtcEZVVkl4Y1RKMFZUUWlmUSMwIiwidHlwIjoiSldUIn0.eyJleHAiOjM2MDEyOTM0ODYsImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbHBOYmt0aGRYZERkbWx3YUd4V1NFVXRjVlZ0T1VveVQyWk5RWE5JVkc5cFNtcEZVVkl4Y1RKMFZUUWlmUSIsImp0aSI6ImFiY2QxMjMiLCJuYmYiOjE3MDkxMzM0ODYsInN1YiI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbGcwY0MxSk5qTnlYM1JZTVhsUVdXTXdjMFZZV1VwVWRuQTFOWFE0VVRkVU4yMWpaVmhhZVd4V1VtOGlmUSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiU3RyZWV0Q3JlZGVudGlhbCJdLCJpc3N1ZXIiOiIiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJmaXJzdE5hbWUiOiJSYW5keSIsImxhc3ROYW1lIjoiTWNSYW5kbyJ9LCJpc3N1YW5jZURhdGUiOiIifX0.VeXTzzoy8krwdg5-6zBqDueZ0l5RH3EwdhMO4n9BQ8ba8qe3Xx6nemnrLHpTYI7glMVg-ynTHugCgQXsspKsAg",
errors: true,
}, {
description: "expired",
input: "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbUUxVDA4NVl6TjVVVXd0UW5RNWIwMXpSM1JEYUdWck5tdE9OVXAyUkU4d2VGRXRlVkpuTjBOYWNXTWlmUSMwIiwidHlwIjoiSldUIn0.eyJleHAiOjE3MDkxMzM1OTcsImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbUUxVDA4NVl6TjVVVXd0UW5RNWIwMXpSM1JEYUdWck5tdE9OVXAyUkU4d2VGRXRlVkpuTjBOYWNXTWlmUSIsImp0aSI6ImFiY2QxMjMiLCJuYmYiOjE3MDkxMzM1OTYsInN1YiI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJa04yZFhCMFpVaHBMWE5OUW1wb1N6Tk1VbXBrYW1OeFVVTkRjVWxzTTB4cGNIVmlNMUpYWVc5NmJsVWlmUSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiZmlyc3ROYW1lIjoiUmFuZHkiLCJsYXN0TmFtZSI6Ik1jUmFuZG8ifSwiaXNzdWFuY2VEYXRlIjoiIn19.BhKQc5Q96ZAISp_qJ0qni0NfZhde3Z0A9hJET8Twhzu2XNA89OgHu8lKyp0M9Fj8WGFVZGOrpfnZtoA13mKMAQ",
errors: true,
}, {
description: "legit",
input: "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbGhCWmpkZmJWOXJZa2RsWVVGcVh6Wm9RMnBWVGtOaFRGUmlRek5DY1ZkaE5UWkVSSG93YlhwTVVYTWlmUSMwIiwidHlwIjoiSldUIn0.eyJleHAiOjI2NTUyMTM2MzksImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbGhCWmpkZmJWOXJZa2RsWVVGcVh6Wm9RMnBWVGtOaFRGUmlRek5DY1ZkaE5UWkVSSG93YlhwTVVYTWlmUSIsImp0aSI6ImFiY2QxMjMiLCJuYmYiOjE3MDkxMzM2MzksInN1YiI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbXROUmxWVVUwWlpia0pEUVZoRldVZEdXalozWjNSVFdWWTNlblF0Y0ZORlpVdHNOMU52YVROclgyTWlmUSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiZmlyc3ROYW1lIjoiUmFuZHkiLCJsYXN0TmFtZSI6Ik1jUmFuZG8ifSwiaXNzdWFuY2VEYXRlIjoiIn19.l3N5-G9sMInwzjQCFhyYNwjRMRd9ojdgsQEsH8S_reYShP_H0BwLbzHSXkIbeFMVTivUziMzOhq0pMJTr-0sAw",
errors: false,
},
}
for _, tt := range vectors {
t.Run(tt.description, func(t *testing.T) {
_, err := vc.Verify[vc.Claims](tt.input)
if tt.errors == true {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestVector_Decode(t *testing.T) {
testVectors, err :=
web5.LoadTestVectors[string, any]("../web5-spec/test-vectors/vc_jwt/decode.json")
assert.NoError(t, err)
fmt.Println("Running test vectors: ", testVectors.Description)
for _, vector := range testVectors.Vectors {
t.Run(vector.Description, func(t *testing.T) {
fmt.Println("Running test vector: ", vector.Description)
_, err := vc.Decode[vc.Claims](vector.Input)
if vector.Errors {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestVector_Verify(t *testing.T) {
testVectors, err :=
web5.LoadTestVectors[string, any]("../web5-spec/test-vectors/vc_jwt/verify.json")
assert.NoError(t, err)
fmt.Println("Running test vectors: ", testVectors.Description)
for _, vector := range testVectors.Vectors {
t.Run(vector.Description, func(t *testing.T) {
fmt.Println("Running test vector: ", vector.Description)
_, err := vc.Verify[vc.Claims](vector.Input)
if vector.Errors {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}