cli: change the module name of the cli (#1431)
This commit is contained in:
172
cli/pkg/web5/crypto/README.md
Normal file
172
cli/pkg/web5/crypto/README.md
Normal 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`
|
||||
6
cli/pkg/web5/crypto/doc.go
Normal file
6
cli/pkg/web5/crypto/doc.go
Normal 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
|
||||
108
cli/pkg/web5/crypto/dsa/dsa.go
Normal file
108
cli/pkg/web5/crypto/dsa/dsa.go
Normal 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)
|
||||
}
|
||||
}
|
||||
150
cli/pkg/web5/crypto/dsa/dsa_test.go
Normal file
150
cli/pkg/web5/crypto/dsa/dsa_test.go
Normal 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)
|
||||
}
|
||||
115
cli/pkg/web5/crypto/dsa/ecdsa/ecdsa.go
Normal file
115
cli/pkg/web5/crypto/dsa/ecdsa/ecdsa.go
Normal 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)
|
||||
}
|
||||
}
|
||||
151
cli/pkg/web5/crypto/dsa/ecdsa/secp256k1.go
Normal file
151
cli/pkg/web5/crypto/dsa/ecdsa/secp256k1.go
Normal 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
|
||||
}
|
||||
99
cli/pkg/web5/crypto/dsa/ecdsa/secp256k1_test.go
Normal file
99
cli/pkg/web5/crypto/dsa/ecdsa/secp256k1_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
82
cli/pkg/web5/crypto/dsa/eddsa/ed25519.go
Normal file
82
cli/pkg/web5/crypto/dsa/eddsa/ed25519.go
Normal 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
|
||||
}
|
||||
68
cli/pkg/web5/crypto/dsa/eddsa/ed25519_test.go
Normal file
68
cli/pkg/web5/crypto/dsa/eddsa/ed25519_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
116
cli/pkg/web5/crypto/dsa/eddsa/eddsa.go
Normal file
116
cli/pkg/web5/crypto/dsa/eddsa/eddsa.go
Normal 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)
|
||||
}
|
||||
}
|
||||
44
cli/pkg/web5/crypto/entropy.go
Normal file
44
cli/pkg/web5/crypto/entropy.go
Normal 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
|
||||
}
|
||||
58
cli/pkg/web5/crypto/entropy_test.go
Normal file
58
cli/pkg/web5/crypto/entropy_test.go
Normal 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)
|
||||
}
|
||||
117
cli/pkg/web5/crypto/keymanager.go
Normal file
117
cli/pkg/web5/crypto/keymanager.go
Normal 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
|
||||
}
|
||||
51
cli/pkg/web5/crypto/keymanager_test.go
Normal file
51
cli/pkg/web5/crypto/keymanager_test.go
Normal 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")
|
||||
}
|
||||
BIN
cli/pkg/web5/diagrams/dids-pkg.png
Normal file
BIN
cli/pkg/web5/diagrams/dids-pkg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 456 KiB |
283
cli/pkg/web5/dids/README.md
Normal file
283
cli/pkg/web5/dids/README.md
Normal 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:
|
||||
|
||||

|
||||
|
||||
|
||||
## 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`
|
||||
113
cli/pkg/web5/dids/did/bearerdid.go
Normal file
113
cli/pkg/web5/dids/did/bearerdid.go
Normal 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
|
||||
}
|
||||
69
cli/pkg/web5/dids/did/bearerdid_test.go
Normal file
69
cli/pkg/web5/dids/did/bearerdid_test.go
Normal 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")
|
||||
}
|
||||
170
cli/pkg/web5/dids/did/did.go
Normal file
170
cli/pkg/web5/dids/did/did.go
Normal 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
|
||||
}
|
||||
193
cli/pkg/web5/dids/did/did_test.go
Normal file
193
cli/pkg/web5/dids/did/did_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
22
cli/pkg/web5/dids/did/portabledid.go
Normal file
22
cli/pkg/web5/dids/did/portabledid.go
Normal 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"`
|
||||
}
|
||||
297
cli/pkg/web5/dids/didcore/document.go
Normal file
297
cli/pkg/web5/dids/didcore/document.go
Normal 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"`
|
||||
}
|
||||
44
cli/pkg/web5/dids/didcore/document_test.go
Normal file
44
cli/pkg/web5/dids/didcore/document_test.go
Normal 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)
|
||||
}
|
||||
99
cli/pkg/web5/dids/didcore/resolution.go
Normal file
99
cli/pkg/web5/dids/didcore/resolution.go
Normal 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
|
||||
}
|
||||
139
cli/pkg/web5/dids/didkey/didkey.go
Normal file
139
cli/pkg/web5/dids/didkey/didkey.go
Normal 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
|
||||
}
|
||||
44
cli/pkg/web5/dids/didkey/key.go
Normal file
44
cli/pkg/web5/dids/didkey/key.go
Normal 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
|
||||
}
|
||||
71
cli/pkg/web5/dids/resolver.go
Normal file
71
cli/pkg/web5/dids/resolver.go
Normal 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
46
cli/pkg/web5/jwk/jwk.go
Normal 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
|
||||
}
|
||||
1
cli/pkg/web5/jwk/jwk_test.go
Normal file
1
cli/pkg/web5/jwk/jwk_test.go
Normal file
@@ -0,0 +1 @@
|
||||
package jwk_test
|
||||
149
cli/pkg/web5/jws/README.md
Normal file
149
cli/pkg/web5/jws/README.md
Normal 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
|
||||
224
cli/pkg/web5/jws/checkjws.go
Normal file
224
cli/pkg/web5/jws/checkjws.go
Normal 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
306
cli/pkg/web5/jws/jws.go
Normal 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
|
||||
}
|
||||
299
cli/pkg/web5/jws/jws_test.go
Normal file
299
cli/pkg/web5/jws/jws_test.go
Normal 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)
|
||||
}
|
||||
79
cli/pkg/web5/jwt/README.md
Normal file
79
cli/pkg/web5/jwt/README.md
Normal 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
286
cli/pkg/web5/jwt/jwt.go
Normal 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
|
||||
144
cli/pkg/web5/jwt/jwt_test.go
Normal file
144
cli/pkg/web5/jwt/jwt_test.go
Normal 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
174
cli/pkg/web5/pexv2/pd.go
Normal 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)
|
||||
}
|
||||
}
|
||||
32
cli/pkg/web5/pexv2/pexv2.go
Normal file
32
cli/pkg/web5/pexv2/pexv2.go
Normal 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
|
||||
}
|
||||
37
cli/pkg/web5/pexv2/pexv2_test.go
Normal file
37
cli/pkg/web5/pexv2/pexv2_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
150
cli/pkg/web5/vc/examples_test.go
Normal file
150
cli/pkg/web5/vc/examples_test.go
Normal 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
246
cli/pkg/web5/vc/vc.go
Normal 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...)
|
||||
}
|
||||
98
cli/pkg/web5/vc/vc_test.go
Normal file
98
cli/pkg/web5/vc/vc_test.go
Normal 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
157
cli/pkg/web5/vc/vcjwt.go
Normal 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
|
||||
}
|
||||
212
cli/pkg/web5/vc/vcjwt_test.go
Normal file
212
cli/pkg/web5/vc/vcjwt_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user