@@ -91,3 +91,89 @@ def get_signing_secret_key_v4(sk, date, region, service):
9191 @staticmethod
9292 def hmac_sha256 (key , msg ):
9393 return hmac .new (key , msg .encode ('utf-8' ), hashlib .sha256 ).digest ()
94+
95+ @staticmethod
96+ def sign_url (path , method , query , ak , sk , region , service , session_token = None , host = None ):
97+ """
98+ Generate presigned URL query string (AWS Signature V4)
99+
100+ :param path: Request path
101+ :param method: HTTP method (GET, POST, etc.)
102+ :param query: Query parameters dict
103+ :param ak: Access Key
104+ :param sk: Secret Key
105+ :param region: Service region
106+ :param service: Service name
107+ :param session_token: Optional session token
108+ :param host: Optional host header to sign
109+ :return: Query string with signature
110+ """
111+ format_date = datetime .datetime .utcnow ().strftime ("%Y%m%dT%H%M%SZ" )
112+ date = format_date [:8 ]
113+
114+ # Build credential scope
115+ credential_scope = '/' .join ([date , region , service , 'request' ])
116+
117+ # Determine if host header should be signed
118+ sign_host = host is not None and host != ''
119+
120+ # Add required query parameters
121+ query = dict (query ) # Make a copy to avoid modifying original
122+ query ['X-Date' ] = format_date
123+ query ['X-NotSignBody' ] = ''
124+ query ['X-Credential' ] = ak + '/' + credential_scope
125+ query ['X-Algorithm' ] = 'HMAC-SHA256'
126+ query ['X-SignedHeaders' ] = 'host' if sign_host else ''
127+ query ['X-SignedQueries' ] = ''
128+
129+ # Generate X-SignedQueries BEFORE adding X-Security-Token
130+ query ['X-SignedQueries' ] = ';' .join (sorted (query .keys ()))
131+ signed_query_keys = set (query .keys ())
132+
133+ # X-Security-Token must be added AFTER X-SignedQueries calculation
134+ if session_token :
135+ query ['X-Security-Token' ] = session_token
136+
137+ # Build canonical request
138+ body_hash = hashlib .sha256 (b'' ).hexdigest ()
139+ canonical_query_params = {k : v for k , v in query .items () if k in signed_query_keys }
140+
141+ if sign_host :
142+ canonical_request = '\n ' .join ([
143+ method ,
144+ path ,
145+ SignerV4 .canonical_query (canonical_query_params ),
146+ 'host:' + host + '\n ' ,
147+ 'host' ,
148+ body_hash
149+ ])
150+ else :
151+ canonical_request = '\n ' .join ([
152+ method ,
153+ path ,
154+ SignerV4 .canonical_query (canonical_query_params ),
155+ '\n ' ,
156+ '' ,
157+ body_hash
158+ ])
159+ sdk_core_logger .debug_sign ("[sign_url] canonical_request:\n %s" , canonical_request )
160+
161+ # Build string to sign
162+ signing_str = '\n ' .join ([
163+ 'HMAC-SHA256' ,
164+ format_date ,
165+ credential_scope ,
166+ hashlib .sha256 (canonical_request .encode ('utf-8' )).hexdigest ()
167+ ])
168+ sdk_core_logger .debug_sign ("[sign_url] string_to_sign:\n %s" , signing_str )
169+
170+ # Calculate signature
171+ signing_key = SignerV4 .get_signing_secret_key_v4 (sk , date , region , service )
172+ signature = hmac .new (signing_key , signing_str .encode ('utf-8' ), hashlib .sha256 ).hexdigest ()
173+ sdk_core_logger .debug_sign ("[sign_url] calculated signature: %s" , signature )
174+
175+ # Add signature to query
176+ query ['X-Signature' ] = signature
177+
178+ # Return encoded query string
179+ return urlencode (sorted (query .items ()))
0 commit comments