Skip to content

Commit db1f54f

Browse files
committed
feat(didcomm): add W3C DIDComm v2 protocol improvements
- WebSocket transport with gorilla/websocket (didcomm/v2 subprotocol) - P-521 curve support in did:key multicodec decoding - did:peer method support (peer:0 and peer:2 with V/E/A/I/D purpose codes) - Present-proof 3.0 protocol implementation - Pickup 2.0 protocol for offline message retrieval - ExternalSigner bridge for HSM-capable VCSigner to DIDComm JWS
1 parent a282dd5 commit db1f54f

File tree

9 files changed

+1612
-4
lines changed

9 files changed

+1612
-4
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ require (
115115
github.com/golang/snappy v1.0.0 // indirect
116116
github.com/gorilla/context v1.1.2 // indirect
117117
github.com/gorilla/securecookie v1.1.2 // indirect
118+
github.com/gorilla/websocket v1.5.3 // indirect
118119
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.5 // indirect
119120
github.com/hashicorp/go-uuid v1.0.3 // indirect
120121
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
@@ -211,4 +212,5 @@ require (
211212
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 // indirect
212213
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
213214
gopkg.in/yaml.v3 v3.0.1 // indirect
215+
nhooyr.io/websocket v1.8.17 // indirect
214216
)

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pw
177177
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
178178
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
179179
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
180+
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
181+
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
180182
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.5 h1:jP1RStw811EvUDzsUQ9oESqw2e4RqCjSAD9qIL8eMns=
181183
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.5/go.mod h1:WXNBZ64q3+ZUemCMXD9kYnr56H7CgZxDBHCVwstfl3s=
182184
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
@@ -524,3 +526,5 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
524526
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
525527
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
526528
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
529+
nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
530+
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
//go:build didcomm && vc20
2+
3+
// Package crypto provides HSM and external signer support for DIDComm operations.
4+
package crypto
5+
6+
import (
7+
"context"
8+
"crypto"
9+
"crypto/ecdsa"
10+
"crypto/ed25519"
11+
"crypto/elliptic"
12+
"encoding/base64"
13+
"encoding/json"
14+
"fmt"
15+
16+
"github.com/lestrrat-go/jwx/v3/jwa"
17+
"github.com/lestrrat-go/jwx/v3/jwk"
18+
"github.com/lestrrat-go/jwx/v3/jws"
19+
20+
vccrypto "vc/pkg/vc20/crypto"
21+
)
22+
23+
// ExternalSigner wraps a VCSigner for use with DIDComm JWS operations.
24+
// This enables HSM-backed keys and other external signers to be used with DIDComm.
25+
type ExternalSigner struct {
26+
signer vccrypto.VCSigner
27+
kid string
28+
algorithm jwa.SignatureAlgorithm
29+
}
30+
31+
// NewExternalSigner creates a DIDComm-compatible signer from a VCSigner.
32+
// The kid parameter should be the DID URL of the verification method.
33+
func NewExternalSigner(signer vccrypto.VCSigner, kid string) (*ExternalSigner, error) {
34+
alg, err := algorithmForVCSigner(signer)
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
return &ExternalSigner{
40+
signer: signer,
41+
kid: kid,
42+
algorithm: alg,
43+
}, nil
44+
}
45+
46+
// algorithmForVCSigner determines the JWA algorithm from the VCSigner.
47+
func algorithmForVCSigner(signer vccrypto.VCSigner) (jwa.SignatureAlgorithm, error) {
48+
switch signer.Algorithm() {
49+
case "ES256":
50+
return jwa.ES256(), nil
51+
case "ES384":
52+
return jwa.ES384(), nil
53+
case "ES512":
54+
return jwa.ES512(), nil
55+
case "EdDSA", "Ed25519":
56+
return jwa.EdDSA(), nil
57+
default:
58+
return jwa.NoSignature(), fmt.Errorf("unsupported algorithm: %s", signer.Algorithm())
59+
}
60+
}
61+
62+
// Sign creates a JWS using the external signer.
63+
func (es *ExternalSigner) Sign(ctx context.Context, plaintext []byte, opts SignOptions) ([]byte, error) {
64+
// Create a JWK from the public key for use in the header
65+
pubKeyJWK, err := es.publicJWK()
66+
if err != nil {
67+
return nil, fmt.Errorf("failed to create public key JWK: %w", err)
68+
}
69+
70+
// Build protected header as a map
71+
header := map[string]interface{}{
72+
"alg": es.algorithm.String(),
73+
}
74+
if es.kid != "" {
75+
header["kid"] = es.kid
76+
}
77+
78+
// Create signing input: BASE64URL(header) || '.' || BASE64URL(payload)
79+
headerBytes, err := json.Marshal(header)
80+
if err != nil {
81+
return nil, fmt.Errorf("failed to marshal header: %w", err)
82+
}
83+
84+
headerB64 := base64.RawURLEncoding.EncodeToString(headerBytes)
85+
payloadB64 := ""
86+
if !opts.Detached {
87+
payloadB64 = base64.RawURLEncoding.EncodeToString(plaintext)
88+
}
89+
90+
signingInput := []byte(headerB64 + "." + payloadB64)
91+
92+
// Hash the signing input according to the algorithm
93+
digest, err := hashForAlgorithm(es.algorithm, signingInput)
94+
if err != nil {
95+
return nil, fmt.Errorf("failed to hash signing input: %w", err)
96+
}
97+
98+
// Sign using the external signer
99+
signature, err := es.signer.SignDigest(ctx, digest)
100+
if err != nil {
101+
return nil, fmt.Errorf("external signer failed: %w", err)
102+
}
103+
104+
// Encode signature
105+
sigB64 := base64.RawURLEncoding.EncodeToString(signature)
106+
107+
// Return compact serialization: header.payload.signature
108+
result := headerB64 + "." + payloadB64 + "." + sigB64
109+
110+
// Verify the signature works by using the public key
111+
_, err = jws.Verify([]byte(result), jws.WithKey(es.algorithm, pubKeyJWK))
112+
if err != nil {
113+
return nil, fmt.Errorf("signature verification failed: %w", err)
114+
}
115+
116+
return []byte(result), nil
117+
}
118+
119+
// hashForAlgorithm computes the appropriate hash for the signing algorithm.
120+
func hashForAlgorithm(alg jwa.SignatureAlgorithm, data []byte) ([]byte, error) {
121+
var hashFunc crypto.Hash
122+
123+
switch alg {
124+
case jwa.ES256():
125+
hashFunc = crypto.SHA256
126+
case jwa.ES384():
127+
hashFunc = crypto.SHA384
128+
case jwa.ES512():
129+
hashFunc = crypto.SHA512
130+
case jwa.EdDSA():
131+
// EdDSA doesn't use pre-hashing - return the data as-is
132+
return data, nil
133+
default:
134+
return nil, fmt.Errorf("unsupported algorithm for hashing: %s", alg)
135+
}
136+
137+
if !hashFunc.Available() {
138+
return nil, fmt.Errorf("hash function not available: %s", hashFunc)
139+
}
140+
141+
h := hashFunc.New()
142+
h.Write(data)
143+
return h.Sum(nil), nil
144+
}
145+
146+
// publicJWK creates a JWK from the signer's public key.
147+
func (es *ExternalSigner) publicJWK() (jwk.Key, error) {
148+
pubKey := es.signer.PublicKey()
149+
150+
switch pk := pubKey.(type) {
151+
case *ecdsa.PublicKey:
152+
return jwk.Import(pk)
153+
case ed25519.PublicKey:
154+
return jwk.Import(pk)
155+
default:
156+
return nil, fmt.Errorf("unsupported public key type: %T", pubKey)
157+
}
158+
}
159+
160+
// KeyID returns the key identifier.
161+
func (es *ExternalSigner) KeyID() string {
162+
return es.kid
163+
}
164+
165+
// Algorithm returns the JWA signature algorithm.
166+
func (es *ExternalSigner) Algorithm() jwa.SignatureAlgorithm {
167+
return es.algorithm
168+
}
169+
170+
// PublicKey returns the public key.
171+
func (es *ExternalSigner) PublicKey() crypto.PublicKey {
172+
return es.signer.PublicKey()
173+
}
174+
175+
// SignMessage wraps Sign with DIDComm standard options.
176+
// This is a convenience method for typical DIDComm signing.
177+
func (es *ExternalSigner) SignMessage(ctx context.Context, message []byte) ([]byte, error) {
178+
return es.Sign(ctx, message, SignOptions{})
179+
}
180+
181+
// VCSignerAdapter wraps a pki.RawSigner to implement VCSigner.
182+
// This enables HSM keys configured through pki to be used with both VC20 and DIDComm.
183+
type VCSignerAdapter struct {
184+
algorithm string
185+
pubKey crypto.PublicKey
186+
signFn func(ctx context.Context, digest []byte) ([]byte, error)
187+
}
188+
189+
// NewVCSignerFromRawSigner creates a VCSigner from a pki.RawSigner.
190+
// This bridges the pki HSM configuration to the VCSigner interface.
191+
func NewVCSignerFromECDSA(key *ecdsa.PrivateKey) vccrypto.VCSigner {
192+
return vccrypto.NewECDSAKeyWrapper(key)
193+
}
194+
195+
// NewVCSignerFromEdDSA creates a VCSigner from an Ed25519 private key.
196+
func NewVCSignerFromEdDSA(key ed25519.PrivateKey) vccrypto.VCSigner {
197+
return vccrypto.NewEdDSAKeyWrapper(key)
198+
}
199+
200+
// Algorithms returns the JWA algorithm for a given curve.
201+
func ECDSAAlgorithmForCurve(curve elliptic.Curve) string {
202+
switch curve {
203+
case elliptic.P256():
204+
return "ES256"
205+
case elliptic.P384():
206+
return "ES384"
207+
case elliptic.P521():
208+
return "ES512"
209+
default:
210+
return ""
211+
}
212+
}

0 commit comments

Comments
 (0)