Skip to content

Commit a282dd5

Browse files
authored
Merge pull request SUNET#263 from sirosfoundation/feature/vc20-keymgmt
Feature/vc20 keymgmt
2 parents 1a2091d + 593889a commit a282dd5

26 files changed

+893
-222
lines changed

cmd/vc20-test-server/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ func handleIssue(w http.ResponseWriter, r *http.Request) {
161161
ProofPurpose: "assertionMethod",
162162
Created: time.Now().UTC(),
163163
}
164-
signedCred, err = suite.Sign(cred, key, opts)
164+
signedCred, err = suite.Sign(r.Context(), cred, key, opts)
165165
case "ecdsa-sd-2023":
166166
suite := vc_ecdsa.NewSdSuite()
167167
opts := &vc_ecdsa.SdSignOptions{

internal/issuer/apiv1/handlers_vc20.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func (c *Client) MakeVC20(ctx context.Context, req *CreateVC20Request) (*CreateV
127127
}
128128

129129
// Sign the credential
130-
signedCred, err := c.signVC20Credential(cred, cryptosuite, req.MandatoryPointers)
130+
signedCred, err := c.signVC20Credential(ctx, cred, cryptosuite, req.MandatoryPointers)
131131
if err != nil {
132132
return nil, fmt.Errorf("failed to sign credential: %w", err)
133133
}
@@ -190,7 +190,7 @@ func (c *Client) buildVC20CredentialJSON(
190190
}
191191

192192
// signVC20Credential signs a credential using the specified cryptosuite
193-
func (c *Client) signVC20Credential(cred *credential.RDFCredential, cryptosuite string, mandatoryPointers []string) (*credential.RDFCredential, error) {
193+
func (c *Client) signVC20Credential(ctx context.Context, cred *credential.RDFCredential, cryptosuite string, mandatoryPointers []string) (*credential.RDFCredential, error) {
194194
// Get the verification method from config
195195
verificationMethod := c.cfg.Issuer.JWTAttribute.Issuer + "#key-1"
196196
if c.kid != "" {
@@ -204,7 +204,7 @@ func (c *Client) signVC20Credential(cred *credential.RDFCredential, cryptosuite
204204
return nil, fmt.Errorf("ecdsa-rdfc-2019 requires ECDSA private key, got %T", c.privateKey)
205205
}
206206
suite := ecdsaSuite.NewSuite()
207-
return suite.Sign(cred, key, &ecdsaSuite.SignOptions{
207+
return suite.Sign(ctx, cred, key, &ecdsaSuite.SignOptions{
208208
VerificationMethod: verificationMethod,
209209
ProofPurpose: "assertionMethod",
210210
Created: time.Now().UTC(),

pkg/didcomm/crypto/ecdh1pu.go

Lines changed: 38 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@ func (e *ECDH1PUKeyAgreement) GenerateEphemeralKey() error {
109109
// Returns the derived key and the ephemeral public key (for inclusion in JWE header).
110110
// Note: This method does NOT include the cc_tag. Use DeriveKeyWithTag for ECDH-1PU key commitment.
111111
func (e *ECDH1PUKeyAgreement) DeriveKey() ([]byte, jwk.Key, error) {
112+
return e.DeriveKeyWithTag(nil)
113+
}
114+
115+
// DeriveKeyWithTag derives the key using ECDH-1PU with optional key commitment (cc_tag).
116+
// If ccTag is nil or empty, uses standard Concat KDF.
117+
// If ccTag is provided, includes the content ciphertext authentication tag in the KDF
118+
// per draft-madden-jose-ecdh-1pu for key commitment.
119+
// Returns the derived key and the ephemeral public key.
120+
func (e *ECDH1PUKeyAgreement) DeriveKeyWithTag(ccTag []byte) ([]byte, jwk.Key, error) {
112121
if e.SenderPrivateKey == nil {
113122
return nil, nil, fmt.Errorf("%w: sender private key is required for ECDH-1PU encryption", ErrInvalidKey)
114123
}
@@ -119,48 +128,22 @@ func (e *ECDH1PUKeyAgreement) DeriveKey() ([]byte, jwk.Key, error) {
119128
}
120129
}
121130

122-
// Extract raw keys for ECDH operations
123-
ephemeralPriv, err := extractECDHPrivateKey(e.EphemeralPrivateKey)
124-
if err != nil {
125-
return nil, nil, fmt.Errorf("failed to extract ephemeral private key: %w", err)
126-
}
127-
128-
senderPriv, err := extractECDHPrivateKey(e.SenderPrivateKey)
129-
if err != nil {
130-
return nil, nil, fmt.Errorf("failed to extract sender private key: %w", err)
131-
}
132-
133-
recipientPub, err := extractECDHPublicKey(e.RecipientPublicKey)
131+
// Compute the shared secret Z = Z_es || Z_ss
132+
z, err := e.computeSharedSecret()
134133
if err != nil {
135-
return nil, nil, fmt.Errorf("failed to extract recipient public key: %w", err)
136-
}
137-
138-
// Compute Z_es = ECDH(ephemeral_private, recipient_public)
139-
zES, err := ephemeralPriv.ECDH(recipientPub)
140-
if err != nil {
141-
return nil, nil, fmt.Errorf("failed to compute Z_es: %w", err)
142-
}
143-
144-
// Compute Z_ss = ECDH(sender_private, recipient_public)
145-
zSS, err := senderPriv.ECDH(recipientPub)
146-
if err != nil {
147-
return nil, nil, fmt.Errorf("failed to compute Z_ss: %w", err)
134+
return nil, nil, err
148135
}
149136

150-
// Concatenate: Z = Z_es || Z_ss
151-
z := append(zES, zSS...)
152-
153137
// Get the required key size for the content encryption algorithm
154138
keySize, err := getKeyWrapKeySize(e.Algorithm)
155139
if err != nil {
156140
return nil, nil, err
157141
}
158142

159-
// Derive the key wrapping key using Concat KDF
160-
// For ECDH-1PU+A256KW, we use the same KDF as ECDH-ES
161-
derivedKey, err := concatKDF(z, e.Algorithm, e.APU, e.APV, keySize)
143+
// Derive the key wrapping key using the appropriate KDF
144+
derivedKey, err := e.deriveKeyFromZ(z, ccTag, keySize)
162145
if err != nil {
163-
return nil, nil, fmt.Errorf("failed to derive key: %w", err)
146+
return nil, nil, err
164147
}
165148

166149
// Get ephemeral public key for inclusion in JWE header
@@ -172,70 +155,56 @@ func (e *ECDH1PUKeyAgreement) DeriveKey() ([]byte, jwk.Key, error) {
172155
return derivedKey, ephemeralPubKey, nil
173156
}
174157

175-
// DeriveKeyWithTag derives the key using ECDH-1PU with key commitment (cc_tag).
176-
// This includes the content ciphertext authentication tag in the KDF per draft-madden-jose-ecdh-1pu.
177-
// Returns the derived key and the ephemeral public key.
178-
func (e *ECDH1PUKeyAgreement) DeriveKeyWithTag(ccTag []byte) ([]byte, jwk.Key, error) {
179-
if e.SenderPrivateKey == nil {
180-
return nil, nil, fmt.Errorf("%w: sender private key is required for ECDH-1PU encryption", ErrInvalidKey)
181-
}
182-
183-
if e.EphemeralPrivateKey == nil {
184-
if err := e.GenerateEphemeralKey(); err != nil {
185-
return nil, nil, err
186-
}
187-
}
188-
158+
// computeSharedSecret computes Z = Z_es || Z_ss for ECDH-1PU encryption.
159+
func (e *ECDH1PUKeyAgreement) computeSharedSecret() ([]byte, error) {
189160
// Extract raw keys for ECDH operations
190161
ephemeralPriv, err := extractECDHPrivateKey(e.EphemeralPrivateKey)
191162
if err != nil {
192-
return nil, nil, fmt.Errorf("failed to extract ephemeral private key: %w", err)
163+
return nil, fmt.Errorf("failed to extract ephemeral private key: %w", err)
193164
}
194165

195166
senderPriv, err := extractECDHPrivateKey(e.SenderPrivateKey)
196167
if err != nil {
197-
return nil, nil, fmt.Errorf("failed to extract sender private key: %w", err)
168+
return nil, fmt.Errorf("failed to extract sender private key: %w", err)
198169
}
199170

200171
recipientPub, err := extractECDHPublicKey(e.RecipientPublicKey)
201172
if err != nil {
202-
return nil, nil, fmt.Errorf("failed to extract recipient public key: %w", err)
173+
return nil, fmt.Errorf("failed to extract recipient public key: %w", err)
203174
}
204175

205176
// Compute Z_es = ECDH(ephemeral_private, recipient_public)
206177
zES, err := ephemeralPriv.ECDH(recipientPub)
207178
if err != nil {
208-
return nil, nil, fmt.Errorf("failed to compute Z_es: %w", err)
179+
return nil, fmt.Errorf("failed to compute Z_es: %w", err)
209180
}
210181

211182
// Compute Z_ss = ECDH(sender_private, recipient_public)
212183
zSS, err := senderPriv.ECDH(recipientPub)
213184
if err != nil {
214-
return nil, nil, fmt.Errorf("failed to compute Z_ss: %w", err)
185+
return nil, fmt.Errorf("failed to compute Z_ss: %w", err)
215186
}
216187

217188
// Concatenate: Z = Z_es || Z_ss
218-
z := append(zES, zSS...)
219-
220-
// Get the required key size for the content encryption algorithm
221-
keySize, err := getKeyWrapKeySize(e.Algorithm)
222-
if err != nil {
223-
return nil, nil, err
224-
}
189+
return append(zES, zSS...), nil
190+
}
225191

226-
// Derive the key wrapping key using Concat KDF with cc_tag (key commitment)
227-
derivedKey, err := concatKDF1PU(z, e.Algorithm, e.APU, e.APV, ccTag, keySize)
228-
if err != nil {
229-
return nil, nil, fmt.Errorf("failed to derive key with tag: %w", err)
192+
// deriveKeyFromZ derives the key wrapping key from shared secret Z.
193+
// Uses concatKDF1PU if ccTag is provided, otherwise uses standard concatKDF.
194+
func (e *ECDH1PUKeyAgreement) deriveKeyFromZ(z, ccTag []byte, keySize int) ([]byte, error) {
195+
if len(ccTag) > 0 {
196+
derivedKey, err := concatKDF1PU(z, e.Algorithm, e.APU, e.APV, ccTag, keySize)
197+
if err != nil {
198+
return nil, fmt.Errorf("failed to derive key with tag: %w", err)
199+
}
200+
return derivedKey, nil
230201
}
231202

232-
// Get ephemeral public key for inclusion in JWE header
233-
ephemeralPubKey, err := e.EphemeralPrivateKey.PublicKey()
203+
derivedKey, err := concatKDF(z, e.Algorithm, e.APU, e.APV, keySize)
234204
if err != nil {
235-
return nil, nil, fmt.Errorf("failed to get ephemeral public key: %w", err)
205+
return nil, fmt.Errorf("failed to derive key: %w", err)
236206
}
237-
238-
return derivedKey, ephemeralPubKey, nil
207+
return derivedKey, nil
239208
}
240209

241210
// DeriveKeyForDecryption derives the key for decryption given the ephemeral public key.
@@ -281,19 +250,8 @@ func (e *ECDH1PUKeyAgreement) DeriveKeyForDecryption(ephemeralPubKey jwk.Key, se
281250
return nil, err
282251
}
283252

284-
// Derive the key wrapping key
285-
// Use concatKDF1PU if CCTag is set (for ECDH-1PU key commitment)
286-
var derivedKey []byte
287-
if len(e.CCTag) > 0 {
288-
derivedKey, err = concatKDF1PU(z, e.Algorithm, e.APU, e.APV, e.CCTag, keySize)
289-
} else {
290-
derivedKey, err = concatKDF(z, e.Algorithm, e.APU, e.APV, keySize)
291-
}
292-
if err != nil {
293-
return nil, fmt.Errorf("failed to derive key: %w", err)
294-
}
295-
296-
return derivedKey, nil
253+
// Derive the key wrapping key (uses CCTag if set for key commitment)
254+
return e.deriveKeyFromZ(z, e.CCTag, keySize)
297255
}
298256

299257
// generateECDHKey generates an ECDH key pair for the specified curve.

pkg/didcomm/crypto/jwe.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ func encryptA256CBCHS512(plaintext, cek, aad []byte) (ciphertext, tag, iv []byte
491491

492492
ciphertext = make([]byte, len(padded))
493493
//nolint:gosec // RFC 7518 A256CBC-HS512 requires CBC mode - this is authenticated encryption with HMAC-SHA-512
494-
mode := cipher.NewCBCEncrypter(block, iv) //NOSONAR go:S5542
494+
mode := cipher.NewCBCEncrypter(block, iv) //NOSONAR
495495
mode.CryptBlocks(ciphertext, padded)
496496

497497
// Compute HMAC-SHA-512 tag
@@ -634,7 +634,7 @@ func wrapKeyAES(cek, wrappingKey []byte) ([]byte, error) {
634634
copy(b[:8], a)
635635
copy(b[8:], r[i])
636636
//nolint:gosec // RFC 3394 AES Key Wrap - this is a key-wrapping primitive with integrity check
637-
block.Encrypt(b, b) //NOSONAR go:S5542
637+
block.Encrypt(b, b) //NOSONAR
638638

639639
// A = MSB(64, B) ^ t where t = (n*j)+i
640640
t := uint64(n*j + i)
@@ -945,7 +945,7 @@ func decryptA256CBCHS512(ctx context.Context, msg *EncryptedMessage, header *JWE
945945
}
946946

947947
//nolint:gosec // RFC 7518 A256CBC-HS512 - HMAC authentication verified above before decryption
948-
mode := cipher.NewCBCDecrypter(block, iv) //NOSONAR go:S5542
948+
mode := cipher.NewCBCDecrypter(block, iv) //NOSONAR
949949
plaintext := make([]byte, len(ciphertext))
950950
mode.CryptBlocks(plaintext, ciphertext)
951951

@@ -1125,7 +1125,7 @@ func unwrapKeyAES(wrappedKey, wrappingKey []byte) ([]byte, error) {
11251125
copy(b[:8], a)
11261126
copy(b[8:], r[i])
11271127
//nolint:gosec // RFC 3394 AES Key Unwrap - integrity check validates after unwrap
1128-
block.Decrypt(b, b) //NOSONAR go:S5542
1128+
block.Decrypt(b, b) //NOSONAR
11291129

11301130
// A = MSB(64, B)
11311131
copy(a, b[:8])

pkg/didcomm/crypto/jws.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,10 @@ func determineSigningAlgorithm(key jwk.Key, hint string) (jwa.SignatureAlgorithm
156156
if crv.String() == "secp256k1" {
157157
return jwa.ES256K(), nil
158158
}
159-
return jwa.ES256(), fmt.Errorf("%w: unsupported curve %s", ErrUnsupportedAlgorithm, crv)
159+
return jwa.ES256(), fmt.Errorf("%w: unsupported curve %v", ErrUnsupportedAlgorithm, crv)
160160
}
161161
default:
162-
return jwa.EdDSA(), fmt.Errorf("%w: unsupported key type %s", ErrUnsupportedAlgorithm, kty)
162+
return jwa.EdDSA(), fmt.Errorf("%w: unsupported key type %v", ErrUnsupportedAlgorithm, kty)
163163
}
164164
}
165165

pkg/didcomm/mediator_integration_test.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
// - Trust ping through a mediator
1010
//
1111
// To run integration tests:
12-
// go test -tags "didcomm vc20 integration" ./pkg/didcomm/...
12+
//
13+
// go test -tags "didcomm vc20 integration" ./pkg/didcomm/...
1314
//
1415
// For live mediator tests (when available):
15-
// go test -tags "didcomm vc20 integration live" ./pkg/didcomm/...
16+
//
17+
// go test -tags "didcomm vc20 integration live" ./pkg/didcomm/...
1618
package didcomm_test
1719

1820
import (
@@ -49,9 +51,9 @@ const (
4951
testRoutingKeySfx = "#routing-key-1"
5052

5153
// Test error format strings
52-
errBuildRoute = "Failed to build route: %v"
53-
errCreatePing = "Failed to create ping: %v"
54-
errHandlePing = "Failed to handle ping: %v"
54+
errBuildRoute = "Failed to build route: %v"
55+
errCreatePing = "Failed to create ping: %v"
56+
errHandlePing = "Failed to handle ping: %v"
5557
)
5658

5759
// =============================================================================
@@ -266,8 +268,14 @@ func TestTrustPingThroughMockMediator(t *testing.T) {
266268
defer mediator.Close()
267269

268270
// Generate key pairs for sender and recipient
269-
senderPub, senderPriv, _ := ed25519.GenerateKey(rand.Reader)
270-
recipientPub, recipientPriv, _ := ed25519.GenerateKey(rand.Reader)
271+
senderPub, senderPriv, err := ed25519.GenerateKey(rand.Reader)
272+
if err != nil {
273+
t.Fatalf("Failed to generate sender Ed25519 key: %v", err)
274+
}
275+
recipientPub, recipientPriv, err := ed25519.GenerateKey(rand.Reader)
276+
if err != nil {
277+
t.Fatalf("Failed to generate recipient Ed25519 key: %v", err)
278+
}
271279
_ = senderPub
272280
_ = senderPriv
273281
_ = recipientPub

pkg/didcomm/message/attachment.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ type Attachment struct {
3939
}
4040

4141
// AttachmentData represents the actual attachment content.
42-
// Exactly one of JWS, Hash, Links, Base64, or JSON should be set.
42+
// At least one of JWS, Links, Base64, or JSON should be set.
43+
// Multiple formats are allowed per DIDComm spec.
44+
// Additional validation rules are enforced by AttachmentData.Validate.
4345
type AttachmentData struct {
4446
// JWS is a JSON Web Signature wrapping the content.
4547
// Provides integrity and optional authentication.
@@ -71,7 +73,8 @@ func (a *Attachment) Validate() error {
7173
// Works for Base64 and JSON encoded content.
7274
func (a *Attachment) GetBytes() ([]byte, error) {
7375
if a.Data.Base64 != "" {
74-
return base64.URLEncoding.DecodeString(a.Data.Base64)
76+
// Use tolerant decoder to support both padded and unpadded encodings
77+
return a.DecodeBase64()
7578
}
7679
if a.Data.JSON != nil {
7780
return json.Marshal(a.Data.JSON)
@@ -111,7 +114,8 @@ func (a *Attachment) GetJSON(v any) error {
111114
return json.Unmarshal(data, v)
112115
}
113116
if a.Data.Base64 != "" {
114-
data, err := base64.URLEncoding.DecodeString(a.Data.Base64)
117+
// Use tolerant decoder for better interop
118+
data, err := a.DecodeBase64()
115119
if err != nil {
116120
return err
117121
}
@@ -120,7 +124,8 @@ func (a *Attachment) GetJSON(v any) error {
120124
return fmt.Errorf("no JSON data available")
121125
}
122126

123-
// Validate checks that exactly one data format is specified.
127+
// Validate checks that at least one data format is specified.
128+
// DIDComm allows multiple formats, so we only check for minimum presence.
124129
func (d *AttachmentData) Validate() error {
125130
count := 0
126131
if d.JWS != nil {
@@ -144,13 +149,13 @@ func (d *AttachmentData) Validate() error {
144149
return nil
145150
}
146151

147-
// NewBase64Attachment creates an attachment with base64-encoded data.
152+
// NewBase64Attachment creates an attachment with base64url-encoded data (no padding).
148153
func NewBase64Attachment(id string, mediaType string, data []byte) Attachment {
149154
return Attachment{
150155
ID: id,
151156
MediaType: mediaType,
152157
Data: AttachmentData{
153-
Base64: base64.URLEncoding.EncodeToString(data),
158+
Base64: base64.RawURLEncoding.EncodeToString(data),
154159
},
155160
}
156161
}

pkg/didcomm/message/doc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
//
2121
// msg := message.New(
2222
// message.WithType("https://example.com/protocols/1.0/hello"),
23-
// message.WithTo([]string{"did:example:bob"}),
23+
// message.WithTo("did:example:bob"),
2424
// message.WithFrom("did:example:alice"),
2525
// message.WithBody(map[string]any{"greeting": "Hello, Bob!"}),
2626
// )

pkg/didcomm/packer.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@ func Pack(ctx context.Context, msg *message.Message, opts PackOptions) (*PackRes
7575
}
7676

7777
// Step 1: Sign if requested
78-
if opts.SignBeforeEncrypt && opts.SignerKey != nil {
78+
if opts.SignBeforeEncrypt {
79+
if opts.SignerKey == nil {
80+
return nil, fmt.Errorf("SignBeforeEncrypt requires SignerKey to be set")
81+
}
7982
signedMsg, err := crypto.Sign(ctx, plaintext, opts.SignerKey, crypto.SignOptions{})
8083
if err != nil {
8184
return nil, fmt.Errorf("failed to sign message: %w", err)

0 commit comments

Comments
 (0)