@@ -6,90 +6,145 @@ package dstack
66
77import (
88 "bytes"
9+ "encoding/binary"
910 "encoding/hex"
11+ "fmt"
1012 "strings"
13+ "time"
1114
1215 "github.com/ethereum/go-ethereum/crypto"
1316 "golang.org/x/crypto/sha3"
1417)
1518
16- // VerifyEnvEncryptPublicKey verifies the signature of a public key.
17- //
18- // Parameters:
19- // - publicKey: The public key bytes to verify (32 bytes)
20- // - signature: The signature bytes (65 bytes)
21- // - appID: The application ID
22- //
23- // Returns the compressed public key if valid, nil otherwise
24- func VerifyEnvEncryptPublicKey (publicKey []byte , signature []byte , appID string ) ([]byte , error ) {
25- if len (signature ) != 65 {
26- return nil , nil
19+ const (
20+ defaultVerifyMaxAgeSeconds uint64 = 300
21+ defaultVerifyFutureSkewSeconds uint64 = 60
22+ )
23+
24+ // VerifyEnvEncryptPublicKeyOptions configures timestamp validation for signature verification.
25+ type VerifyEnvEncryptPublicKeyOptions struct {
26+ MaxAgeSeconds uint64
27+ FutureSkewSeconds uint64
28+ }
29+
30+ func normalizeVerifyOptions (opts * VerifyEnvEncryptPublicKeyOptions ) (maxAgeSeconds uint64 , futureSkewSeconds uint64 ) {
31+ maxAgeSeconds = defaultVerifyMaxAgeSeconds
32+ futureSkewSeconds = defaultVerifyFutureSkewSeconds
33+ if opts == nil {
34+ return
35+ }
36+ if opts .MaxAgeSeconds > 0 {
37+ maxAgeSeconds = opts .MaxAgeSeconds
2738 }
39+ if opts .FutureSkewSeconds > 0 {
40+ futureSkewSeconds = opts .FutureSkewSeconds
41+ }
42+ return
43+ }
2844
29- // Create the message to verify
45+ func buildVerifyMessage ( publicKey [] byte , appID string ) ([] byte , error ) {
3046 prefix := []byte ("dstack-env-encrypt-pubkey" )
31-
32- // Remove 0x prefix if present
47+
3348 cleanAppID := appID
3449 if strings .HasPrefix (appID , "0x" ) {
3550 cleanAppID = appID [2 :]
3651 }
37-
52+
3853 appIDBytes , err := hex .DecodeString (cleanAppID )
3954 if err != nil {
40- return nil , nil
55+ return nil , err
4156 }
42-
57+
4358 separator := []byte (":" )
44-
45- // Construct message: prefix + ":" + app_id + public_key
46- message := bytes .Join ([][]byte {prefix , separator , appIDBytes , publicKey }, nil )
47-
48- // Hash the message with Keccak-256
59+ return bytes .Join ([][]byte {prefix , separator , appIDBytes , publicKey }, nil ), nil
60+ }
61+
62+ func recoverCompressedPublicKey (message []byte , signature []byte ) ([]byte , error ) {
63+ if len (signature ) != 65 {
64+ return nil , nil
65+ }
66+
4967 hasher := sha3 .NewLegacyKeccak256 ()
5068 hasher .Write (message )
5169 messageHash := hasher .Sum (nil )
52-
53- // Extract r, s, v from signature (last byte is recovery id)
70+
5471 r := signature [0 :32 ]
5572 s := signature [32 :64 ]
5673 recovery := signature [64 ]
57-
58- // Create signature in format expected by go-ethereum
74+
5975 sigBytes := make ([]byte , 64 )
6076 copy (sigBytes [0 :32 ], r )
6177 copy (sigBytes [32 :64 ], s )
62-
63- // Recover the public key from the signature
78+
6479 recoveredPubKey , err := crypto .SigToPub (messageHash , append (sigBytes , recovery ))
6580 if err != nil {
6681 return nil , nil
6782 }
68-
69- // Return compressed public key
83+
7084 compressedPubKey := crypto .CompressPubkey (recoveredPubKey )
71-
72- // Add 0x prefix
7385 result := make ([]byte , len (compressedPubKey )+ 2 )
7486 result [0 ] = '0'
7587 result [1 ] = 'x'
7688 copy (result [2 :], []byte (hex .EncodeToString (compressedPubKey )))
77-
89+
7890 return result , nil
7991}
8092
93+ // VerifyEnvEncryptPublicKey verifies the signature of a public key (legacy format without timestamp).
94+ func VerifyEnvEncryptPublicKey (publicKey []byte , signature []byte , appID string ) ([]byte , error ) {
95+ message , err := buildVerifyMessage (publicKey , appID )
96+ if err != nil {
97+ return nil , nil
98+ }
99+ return recoverCompressedPublicKey (message , signature )
100+ }
101+
102+ // VerifyEnvEncryptPublicKeyWithTimestamp verifies a public-key signature with timestamp freshness checks.
103+ //
104+ // Message format:
105+ //
106+ // prefix + ":" + app_id + timestamp_be_u64 + public_key
107+ func VerifyEnvEncryptPublicKeyWithTimestamp (
108+ publicKey []byte ,
109+ signature []byte ,
110+ appID string ,
111+ timestamp uint64 ,
112+ opts * VerifyEnvEncryptPublicKeyOptions ,
113+ ) ([]byte , error ) {
114+ if len (signature ) != 65 {
115+ return nil , nil
116+ }
117+
118+ maxAgeSeconds , futureSkewSeconds := normalizeVerifyOptions (opts )
119+ now := uint64 (time .Now ().Unix ())
120+ if timestamp > now {
121+ if timestamp - now > futureSkewSeconds {
122+ return nil , fmt .Errorf ("timestamp is too far in the future" )
123+ }
124+ } else if now - timestamp > maxAgeSeconds {
125+ return nil , fmt .Errorf ("timestamp is too old: %ds > %ds" , now - timestamp , maxAgeSeconds )
126+ }
127+
128+ baseMessage , err := buildVerifyMessage (publicKey , appID )
129+ if err != nil {
130+ return nil , nil
131+ }
132+
133+ timestampBytes := make ([]byte , 8 )
134+ binary .BigEndian .PutUint64 (timestampBytes , timestamp )
135+ message := append (append ([]byte {}, baseMessage [:len (baseMessage )- len (publicKey )]... ), timestampBytes ... )
136+ message = append (message , publicKey ... )
137+
138+ return recoverCompressedPublicKey (message , signature )
139+ }
140+
81141// VerifySignatureSimple is a simplified version for basic signature verification
82142func VerifySignatureSimple (message []byte , signature []byte , expectedPubKey []byte ) bool {
83143 if len (signature ) != 65 {
84144 return false
85145 }
86-
87- // Hash the message
146+
88147 hash := crypto .Keccak256Hash (message )
89-
90- // Remove recovery ID for verification
91148 sig := signature [:64 ]
92-
93- // Verify signature
94149 return crypto .VerifySignature (expectedPubKey , hash .Bytes (), sig )
95- }
150+ }
0 commit comments