Skip to content

Commit bb434cf

Browse files
committed
Add support for Sign/Verify to SDK
1 parent 4c4a951 commit bb434cf

15 files changed

Lines changed: 851 additions & 18 deletions

File tree

sdk/curl/api.md

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ Generates an ECDSA key using the k256 elliptic curve, derived from the applicati
7171
|-------|------|-------------|----------|
7272
| `path` | string | Path for the key | `"my/key/path"` |
7373
| `purpose` | string | Purpose for the key. Can be any string. This is used in the signature chain. | `"signing"` | `"encryption"` |
74+
| `algorithm` | string | Either `secp256k1` or `ed25519`. Defaults to `secp256k1` | `ed25519` |
7475

7576
**Example:**
7677
```bash
@@ -79,14 +80,15 @@ curl --unix-socket /var/run/dstack.sock -X POST \
7980
-H 'Content-Type: application/json' \
8081
-d '{
8182
"path": "my/key/path",
82-
"purpose": "signing"
83+
"purpose": "signing",
84+
"algorithm": "ed25519",
8385
}'
8486
```
8587

8688
Or
8789

8890
```bash
89-
curl --unix-socket /var/run/dstack.sock http://dstack/GetKey?path=my/key/path&purpose=signing
91+
curl --unix-socket /var/run/dstack.sock http://dstack/GetKey?path=my/key/path&purpose=signing&algorithm=ed25519
9092
```
9193

9294
**Response:**
@@ -189,6 +191,80 @@ curl --unix-socket /var/run/dstack.sock -X POST \
189191
**Response:**
190192
Empty response with HTTP 200 status code on success.
191193

194+
### 6. Sign
195+
196+
Signs a payload.
197+
198+
**Endpoint:** `/Sign`
199+
200+
**Request Parameters:**
201+
202+
| Field | Type | Description | Example |
203+
|-------|------|-------------|----------|
204+
| `algorithm` | string | `ed25519`, `secp256k1_prehashed` or `secp256k1`| `ed25519` |
205+
| `data` | string | Hex-encoded payload data | `deadbeef` |
206+
207+
**Example:**
208+
```bash
209+
curl --unix-socket /var/run/dstack.sock -X POST \
210+
http://dstack/Sign \
211+
-H 'Content-Type: application/json' \
212+
-d '{
213+
"algorithm": "ed25519",
214+
"data": "deadbeef"
215+
}'
216+
```
217+
218+
**Response:**
219+
```json
220+
{
221+
"signature": "<hex-encoded-signature>",
222+
"signature_chain": [
223+
"<hex-encoded-signature-1>",
224+
"<hex-encoded-signature-2>",
225+
"<hex-encoded-signature-3>"
226+
]
227+
"public_key": "<hex-encoded-public-key>"
228+
}
229+
```
230+
231+
### 7. Verify
232+
233+
Verifies a signature.
234+
235+
**Endpoint:** `/Verify`
236+
237+
**Request Parameters:**
238+
239+
| Field | Type | Description | Example |
240+
|-------|------|-------------|----------|
241+
| `algorithm` | string | `ed25519`, `secp256k1_prehashed` or `secp256k1`| `ed25519` |
242+
| `data` | string | Hex-encoded payload data | `deadbeef` |
243+
| `signature` | string | Hex-encoded signature | `deadbeef` |
244+
| `public_key` | string | Hex-encoded public key | `deadbeef` |
245+
246+
**Example:**
247+
```bash
248+
curl --unix-socket /var/run/dstack.sock -X POST \
249+
http://dstack/Verify \
250+
-H 'Content-Type: application/json' \
251+
-d '{
252+
"algorithm": "ed25519",
253+
"data": "deadbeef",
254+
"signature": "deadbeef",
255+
"public_key": "deadbeef"
256+
}'
257+
```
258+
259+
**Response:**
260+
```json
261+
{
262+
"valid": "<true|false>"
263+
}
264+
```
265+
266+
```
267+
192268
## Error Responses
193269

194270
All endpoints may return the following HTTP status codes:

sdk/go/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,11 @@ NOTE: Leave endpoint empty in production. You only need to add `volumes` in your
9191
#### Methods
9292

9393
- `Info(ctx context.Context) (*InfoResponse, error)`: Retrieves information about the CVM instance.
94-
- `GetKey(ctx context.Context, path string, purpose string) (*GetKeyResponse, error)`: Derives a key for the given path and purpose.
94+
- `GetKey(ctx context.Context, path string, purpose string, algorithm string) (*GetKeyResponse, error)`: Derives a key for the given path, purpose and algorithm.
9595
- `GetQuote(ctx context.Context, reportData []byte) (*GetQuoteResponse, error)`: Generates a TDX quote using SHA512 as the hash algorithm.
9696
- `GetTlsKey(ctx context.Context, path string, subject string, altNames []string, usageRaTls bool, usageServerAuth bool, usageClientAuth bool, randomSeed bool) (*GetTlsKeyResponse, error)`: Derives a key for the given path and purpose.
97+
- `Sign(ctx context.Context, algorithm string, data []byte) (*SignResponse, error)`: Signs a payload
98+
- `Verify(ctx context.Context, algorithm string, data []byte, signature []byte, public_key []byte) (*VerifyResponse, error)`: Verifies a payload
9799

98100
## Development
99101

sdk/go/dstack/client.go

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -348,10 +348,11 @@ func (c *DstackClient) GetTlsKey(
348348
}
349349

350350
// Gets a key from the dstack service.
351-
func (c *DstackClient) GetKey(ctx context.Context, path string, purpose string) (*GetKeyResponse, error) {
351+
func (c *DstackClient) GetKey(ctx context.Context, path string, purpose string, algorithm string) (*GetKeyResponse, error) {
352352
payload := map[string]interface{}{
353-
"path": path,
354-
"purpose": purpose,
353+
"path": path,
354+
"purpose": purpose,
355+
"algorithm": algorithm,
355356
}
356357

357358
data, err := c.sendRPCRequest(ctx, "/GetKey", payload)
@@ -422,6 +423,83 @@ func (c *DstackClient) Info(ctx context.Context) (*InfoResponse, error) {
422423
return &response, nil
423424
}
424425

426+
type SignResponse struct {
427+
Signature []byte
428+
SignatureChain [][]byte
429+
PublicKey []byte
430+
}
431+
432+
// Signs a payload.
433+
func (c *DstackClient) Sign(ctx context.Context, algorithm string, data []byte) (*SignResponse, error) {
434+
payload := map[string]interface{}{
435+
"algorithm": algorithm,
436+
"data": hex.EncodeToString(data),
437+
}
438+
439+
respData, err := c.sendRPCRequest(ctx, "/Sign", payload)
440+
if err != nil {
441+
return nil, err
442+
}
443+
444+
var response struct {
445+
Signature string `json:"signature"`
446+
SignatureChain []string `json:"signature_chain"`
447+
PublicKey string `json:"public_key"`
448+
}
449+
if err := json.Unmarshal(respData, &response); err != nil {
450+
return nil, fmt.Errorf("failed to unmarshal sign response: %w", err)
451+
}
452+
453+
sig, err := hex.DecodeString(response.Signature)
454+
if err != nil {
455+
return nil, fmt.Errorf("failed to decode signature: %w", err)
456+
}
457+
pubKey, err := hex.DecodeString(response.PublicKey)
458+
if err != nil {
459+
return nil, fmt.Errorf("failed to decode public key: %w", err)
460+
}
461+
462+
sigChain := make([][]byte, len(response.SignatureChain))
463+
for i, s := range response.SignatureChain {
464+
sigChain[i], err = hex.DecodeString(s)
465+
if err != nil {
466+
return nil, fmt.Errorf("failed to decode signature chain element %d: %w", i, err)
467+
}
468+
}
469+
470+
return &SignResponse{
471+
Signature: sig,
472+
SignatureChain: sigChain,
473+
PublicKey: pubKey,
474+
}, nil
475+
}
476+
477+
type VerifyResponse struct {
478+
Valid bool `json:"valid"`
479+
}
480+
481+
// Verifies a payload.
482+
func (c *DstackClient) Verify(ctx context.Context, algorithm string, data []byte, signature []byte, publicKey []byte) (*VerifyResponse, error) {
483+
payload := map[string]interface{}{
484+
"algorithm": algorithm,
485+
"data": hex.EncodeToString(data),
486+
"signature": hex.EncodeToString(signature),
487+
"public_key": hex.EncodeToString(publicKey),
488+
}
489+
490+
respData, err := c.sendRPCRequest(ctx, "/Verify", payload)
491+
if err != nil {
492+
return nil, err
493+
}
494+
495+
var response VerifyResponse
496+
if err := json.Unmarshal(respData, &response); err != nil {
497+
return nil, fmt.Errorf("failed to unmarshal verify response: %w", err)
498+
}
499+
500+
return &response, nil
501+
}
502+
425503
// EmitEvent sends an event to be extended to RTMR3 on TDX platform.
426504
// The event will be extended to RTMR3 with the provided name and payload.
427505
//

sdk/go/dstack/client_test.go

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package dstack_test
77
import (
88
"bytes"
99
"context"
10+
"crypto/sha256"
1011
"encoding/hex"
1112
"encoding/json"
1213
"fmt"
@@ -25,7 +26,7 @@ import (
2526

2627
func TestGetKey(t *testing.T) {
2728
client := dstack.NewDstackClient()
28-
resp, err := client.GetKey(context.Background(), "/", "test")
29+
resp, err := client.GetKey(context.Background(), "/", "test", "ed25519")
2930
if err != nil {
3031
t.Fatal(err)
3132
}
@@ -434,7 +435,7 @@ func TestGetKeySignatureVerification(t *testing.T) {
434435
client := dstack.NewDstackClient()
435436
path := "/test/path"
436437
purpose := "test-purpose"
437-
resp, err := client.GetKey(context.Background(), path, purpose)
438+
resp, err := client.GetKey(context.Background(), path, purpose, "secp256k1")
438439
if err != nil {
439440
t.Fatal(err)
440441
}
@@ -608,3 +609,111 @@ func compressPublicKey(uncompressedKey []byte) ([]byte, error) {
608609
}
609610
return crypto.CompressPubkey(pubKey), nil
610611
}
612+
613+
func TestSignAndVerifyEd25519(t *testing.T) {
614+
client := dstack.NewDstackClient()
615+
dataToSign := []byte("test message for ed25519")
616+
algorithm := "ed25519"
617+
618+
signResp, err := client.Sign(context.Background(), algorithm, dataToSign)
619+
if err != nil {
620+
t.Fatalf("Sign() error = %v", err)
621+
}
622+
623+
if len(signResp.Signature) == 0 {
624+
t.Error("expected signature to not be empty")
625+
}
626+
if len(signResp.PublicKey) == 0 {
627+
t.Error("expected public key to not be empty")
628+
}
629+
if len(signResp.SignatureChain) != 3 {
630+
t.Errorf("expected signature chain to have 3 elements, got %d", len(signResp.SignatureChain))
631+
}
632+
if !bytes.Equal(signResp.Signature, signResp.SignatureChain[0]) {
633+
t.Error("expected Signature to be the same as SignatureChain[0]")
634+
}
635+
636+
verifyResp, err := client.Verify(context.Background(), algorithm, dataToSign, signResp.Signature, signResp.PublicKey)
637+
if err != nil {
638+
t.Fatalf("Verify() error = %v", err)
639+
}
640+
641+
if !verifyResp.Valid {
642+
t.Error("expected verification to be valid")
643+
}
644+
645+
badData := []byte("wrong message")
646+
verifyResp, err = client.Verify(context.Background(), algorithm, badData, signResp.Signature, signResp.PublicKey)
647+
if err != nil {
648+
t.Fatalf("Verify() with bad data error = %v", err)
649+
}
650+
651+
if verifyResp.Valid {
652+
t.Error("expected verification with bad data to be invalid")
653+
}
654+
}
655+
656+
func TestSignAndVerifySecp256k1(t *testing.T) {
657+
client := dstack.NewDstackClient()
658+
dataToSign := []byte("test message for secp256k1")
659+
algorithm := "secp256k1"
660+
661+
signResp, err := client.Sign(context.Background(), algorithm, dataToSign)
662+
if err != nil {
663+
t.Fatalf("Sign() error = %v", err)
664+
}
665+
666+
if len(signResp.Signature) == 0 {
667+
t.Error("expected signature to not be empty")
668+
}
669+
if len(signResp.PublicKey) == 0 {
670+
t.Error("expected public key to not be empty")
671+
}
672+
if len(signResp.SignatureChain) != 3 {
673+
t.Errorf("expected signature chain to have 3 elements, got %d", len(signResp.SignatureChain))
674+
}
675+
676+
verifyResp, err := client.Verify(context.Background(), algorithm, dataToSign, signResp.Signature, signResp.PublicKey)
677+
if err != nil {
678+
t.Fatalf("Verify() error = %v", err)
679+
}
680+
681+
if !verifyResp.Valid {
682+
t.Error("expected verification to be valid")
683+
}
684+
}
685+
686+
func TestSignAndVerifySecp256k1Prehashed(t *testing.T) {
687+
client := dstack.NewDstackClient()
688+
dataToSign := []byte("test message for secp256k1 prehashed")
689+
digest := sha256.Sum256(dataToSign)
690+
algorithm := "secp256k1_prehashed"
691+
692+
signResp, err := client.Sign(context.Background(), algorithm, digest[:])
693+
if err != nil {
694+
t.Fatalf("Sign() error = %v", err)
695+
}
696+
697+
if len(signResp.Signature) == 0 {
698+
t.Error("expected signature to not be empty")
699+
}
700+
701+
verifyResp, err := client.Verify(context.Background(), algorithm, digest[:], signResp.Signature, signResp.PublicKey)
702+
if err != nil {
703+
t.Fatalf("Verify() error = %v", err)
704+
}
705+
706+
if !verifyResp.Valid {
707+
t.Error("expected verification to be valid")
708+
}
709+
710+
// Test invalid digest length for signing
711+
invalidDigest := []byte{1, 2, 3}
712+
_, err = client.Sign(context.Background(), algorithm, invalidDigest)
713+
if err == nil {
714+
t.Fatal("expected error for invalid digest length, got nil")
715+
}
716+
if !strings.Contains(err.Error(), "32-byte digest") {
717+
t.Errorf("expected error to mention '32-byte digest', got: %v", err)
718+
}
719+
}

0 commit comments

Comments
 (0)