Updated on 2024-09-29 GMT+08:00

Using an Authorization Header

Function

Using a header for authentication is the most common method of carrying a signature, supported by all OBS APIs. With this method, the signature is carried in the Authorization header of a request, as shown in Figure 1.

Figure 1 An Authorization header

The header format is:

Authorization: OBS AccessKeyID:Signature

An OBS string, an access key ID (AK), and a signature comprise an Authorization header. The string OBS and AK are separated by a space, and the AK and signature are separated by a colon (:).

  • To obtain an access key ID (AK), see Access Keys.
  • To calculate a signature, see Table 1.
    Table 1 Calculating a signature carried in the Authorization header

    Method

    Description

    Link

    Using SDKs

    All available OBS SDKs provide automatic calculation. Save time by using them directly.

    Using SDKs

    Using signature generators

    OBS provides graphical tools to make it easier to generate signatures.

    Using a Signature Generator

    Manual calculation

    You can manually calculate a signature using the provided signing algorithm.

    Manually Calculating a Signature

Using SDKs

OBS SDKs provide built-in signature calculation, so you do not need to take care of this operation. Table 2 lists the source code files for signature calculation for OBS SDKs in different languages.

Table 2 Signature source files of OBS SDKs

Using SDKs

Signature Source File

Java

RestStorageService.java

Python

auth.py

Go

auth.go

C

request.c

Node.Js

utils.js

Browser.Js

utils.js

PHP

DefaultSignature.php

.NET

Signer.cs

Using a Signature Generator

OBS provides a graphical tool to make it easier to generate signatures. You can find the tool here. To learn how to use the tool, see Using Signature Generators.

Manually Calculating a Signature

Use the following algorithm to calculate the signature carried in the header:

Signature = Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) )

The following figure shows how the signature carried in the header is calculated:

Figure 2 Calculating a signature carried in the header
  1. Construct a StringToSign. Below are the StringToSign structure and parameters. For more StringToSign examples, see Example StringToSign.

    StringToSign = 
        HTTP-Verb + "\n" + 
        Content-MD5 + "\n" + 
        Content-Type + "\n" + 
        Date + "\n" + 
        CanonicalizedHeaders + "\n" +
        CanonicalizedResource
    Table 3 Parameters required for constructing a StringToSign

    Parameter

    Type

    Mandatory (Yes/No)

    Description

    HTTP-Verb

    String

    Yes

    Explanation:

    The HTTP method used to make a request (also called an operation). For RESTful APIs, HTTP methods include PUT, GET, DELETE, and other operations. Select a method based on the API to be called.

    Restrictions:

    None

    Value range:

    • GET: Requests that a server return a specific resource, for example, obtaining a bucket list or downloading an object.
    • PUT: Requests that a server update a specific resource, for example, creating a bucket or uploading an object.
    • POST: Requests that a server add a resource or perform special operations such as initiating a multipart upload or assembling parts.
    • DELETE: Requests that a server delete a specific resource such as an object.
    • HEAD: Requests that a server return the description of a specific resource, for example, obtaining object metadata.
    • OPTIONS (not supported for signature generators): Requests that a server check whether the user has the permissions to perform an operation on a resource. CORS must be configured for the bucket.

    Default value:

    None

    Content-MD5

    String

    No

    Explanation:

    The base64-encoded 128-bit MD5 digest of the request body based on RFC 1864.

    Restrictions:

    None

    Value range:

    A string of 22 characters.

    Default value:

    This parameter is left blank by default.

    Content-Type

    String

    No

    Explanation:

    The file type of an object—for example, text/plain—which determines what format and encoding a browser uses to read the file.

    Restrictions:

    None

    Value range:

    See What Is Content-Type (MIME)?

    Default value:

    If this parameter is not included, an empty string is used by default. Table 4 shows an example.

    Date

    String

    Yes

    Explanation:

    When a request was made. If the value of Date is over 15 minutes away from the current server time, the server returns 403, indicating that the request is invalid.

    Restrictions:

    • The value must be a GMT in RFC 1123 format.
    • If Date and x-obs-date headers are both specified, x-obs-date applies.

    Value range:

    None

    Default value:

    None

    CanonicalizedHeaders

    String

    No

    Explanation:

    Additional headers defined by OBS that include the x-obs- prefix, for example, x-obs-date, x-obs-acl, and x-obs-meta-*. For each additional header, separate its name and value by a colon (:). In x-obs-storage-class:STANDARD, for example, x-obs-storage-class is the header name, and STANDARD is the header value.

    Restrictions:

    1. Header names must be lowercase. The header value is case sensitive. An example header is x-obs-storage-class:STANDARD.
    2. A header name cannot contain non-ASCII or unrecognizable characters, which are also not recommended for header values. If such characters are necessary, they must be encoded or decoded in URL or Base64 on the client side, because the server side does not perform any decoding.
    3. A header cannot contain meaningless tabs or spaces. For example, x-obs-meta-name: name (with a meaningless space before name) must be changed to x-obs-meta-name:name.
    4. If multiple headers are involved, they must be sorted in ascending lexicographic order by header name.
    5. If a header has multiple values, these values need to be written together under their shared header name, separated by commas (,). For example, x-obs-meta-name:name1 and x-obs-meta-name:name2 must be combined into x-obs-meta-name:name1,name2.
    6. Each header requires a new line, and each line ends with a newline character (\n).

    Value range:

    Determined by the API to be called

    Default value:

    This parameter is left blank by default.

    CanonicalizedResource

    String

    Yes

    Explanation:

    OBS resources specified in an HTTP request. The structure is as follows:

    CanonicalizedResource = "/" + bucket-name +  "/" + object-name + "?" + sub-resource 

    For example, if you want to call GetObject to obtain version xxx of object object-test stored in bucket-test and change Content-Type to text/plain, then CanonicalizedResource would be as follows:

    /bucket-test/object-test?response-content-type=text/plain&versionId=xxx
    • bucket-name:

      If the bucket does not have a custom domain name associated, use its own name.

      Otherwise, use its associated custom domain name. In /obs.ccc.com/object, for example, obs.ccc.com is a custom bucket domain name.

      If an API operation does not require a bucket to be specified, for example, listing all buckets under an account, omit both the bucket name and object name by using, for example, /.

    • object-name:

      The name of the required object. Follow the object naming rules.

    • sub-resource: Arrange multiple sub-resources in ascending lexicographic order and use ampersands (&) to separate them.

      sub-resource identifiers: CDNNotifyConfiguration, acl, append, attname, backtosource, cors, customdomain, delete, deletebucket, directcoldaccess, encryption, inventory, length, lifecycle, location, logging, metadata, modify, name, notification, partNumber, policy, position, quota, rename, replication, restore, storageClass, storagePolicy, storageinfo, tagging, torrent, truncate, uploadId, uploads, versionId, versioning, versions, website,x-obs-security-token, object-lock, retention

      Response header sub-resources: response-cache-control, response-content-disposition, response-content-encoding, response-content-language, response-content-type, response-expires

      Image processing sub-resources: x-image-process, x-image-save-bucket, x-image-save-object

    Restrictions:

    A sub-resource usually has only one value. Listing multiple values for the same resource key—for example, key=value1&key=value2—is not recommended. If you do so, only the first sub-resource value is used to calculate the signature.

    Value range:

    None

    Default value:

    If this parameter is not specified, / is used.

  2. UTF-8 encode the result from step 1.
  3. Use your SK to calculate the HMAC-SHA1 of the result from step 2.
  4. Base64 encode the result from step 3 to obtain the signature.

Examples for Calculating Content-MD5 in Java

You can choose to add the Content-MD5 header when constructing the StringToSign. The code example below calculates the Content-MD5 header value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import java.security.MessageDigest;
import sun.misc.BASE64Encoder;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;

public class Md5{
     public static void main(String[] args) {
          try {
                 String exampleString = "blog";
                 MessageDigest messageDigest = MessageDigest.getInstance("MD5"); 
                 BASE64Encoder encoder = new BASE64Encoder(); 
                 // Base64 encode the MD5 value of the string.
                 String contentMd5 = encoder.encode(messageDigest.digest(exampleString.getBytes("utf-8")));
                 System.out.println("Content-MD5:" + contentMd5);    
          } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) 
          {
                 e.printStackTrace();
          }
     }
}
Use hash-based message authentication code (HMAC) to calculate the signature based on the StringToSign and SK:
Signature = Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) )
The following is an example client request for creating a private bucket named newbucketname2 in a given region:
PUT / HTTP/1.1 
Host: newbucketname2.obs.region.myhuaweicloud.com
Content-Length: length
Date: Fri, 06 Jul 2018 03:45:51 GMT
x-obs-acl:private
x-obs-storage-class:STANDARD
Authorization: OBS UDSIAMSTUBTEST000254:ydH8ffpcbS6YpeOMcEZfn0wE90c=
<CreateBucketConfiguration xmlns="http://obs.region.myhuaweicloud.com/doc/2015-06-30/"> 
    <Location>region</Location> 
</CreateBucketConfiguration>

Code Examples

The signing code examples for different languages are as follows:

Java

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class SignDemo {
	
    private static final String SIGN_SEP = "\n";
    private static final String OBS_PREFIX = "x-obs-";
    private static final String DEFAULT_ENCODING = "UTF-8";
    private static final List<String> SUB_RESOURCES = Collections.unmodifiableList(Arrays.asList(
        "CDNNotifyConfiguration", "acl", "append", "attname", "backtosource", "cors", "customdomain", "delete",
   	"deletebucket", "directcoldaccess", "encryption", "inventory", "length", "lifecycle", "location", "logging",
   	"metadata", "mirrorBackToSource", "modify", "name", "notification", "obscompresspolicy", 
   	"partNumber", "policy", "position", "quota","rename", "replication", "response-cache-control", 
   	"response-content-disposition","response-content-encoding", "response-content-language", "response-content-type", 
   	"response-expires","restore", "storageClass", "storagePolicy", "storageinfo", "tagging", "torrent", "truncate",
   	"uploadId", "uploads", "versionId", "versioning", "versions", "website", "x-image-process",
  	 "x-image-save-bucket", "x-image-save-object", "x-obs-security-token", "object-lock", "retention"));
 
    private String ak;
    private String sk;
    // UTF8 encode the character string.
    public String urlEncode(String input) throws UnsupportedEncodingException {
        return URLEncoder.encode(input, DEFAULT_ENCODING)
            .replaceAll("%7E", "~") //for browser
            .replaceAll("%2F", "/")
            .replaceAll("%20", "+");
    }
 
    private String join(List<?> items, String delimiter) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < items.size(); i++) {
            String item = items.get(i).toString();
            sb.append(item);
            if (i < items.size() - 1) {
                sb.append(delimiter);
            }
        }
        return sb.toString();
    }
	
    private boolean isValid(String input) {
        return input != null && !input.equals("");
    }

    // Use the SK to calculate the HmacSHA1.
    public String hmacSha1(String input) throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException {
        SecretKeySpec signingKey = new SecretKeySpec(this.sk.getBytes(DEFAULT_ENCODING), "HmacSHA1");
        // Obtain a Mac instance and use the getInstance method to specify the HMAC-SHA1 for the algorithm.
        Mac mac = Mac.getInstance("HmacSHA1");
        // Use the SK to initialize the Mac object.
        mac.init(signingKey);
        return Base64.getEncoder().encodeToString(mac.doFinal(input.getBytes(DEFAULT_ENCODING)));
    }

    // Construct a StringToSign.
    private String stringToSign(String httpMethod, Map<String, String[]> headers, Map<String, String> queries,
        String bucketName, String objectName) throws Exception{
        String contentMd5 = "";
        String contentType = "";
        String date = "";
		
        TreeMap<String, String> canonicalizedHeaders = new TreeMap<String, String>();
		
        String key;
        List<String> temp = new ArrayList<String>();
        for(Map.Entry<String, String[]> entry : headers.entrySet()) {
            key = entry.getKey();
            if(key == null || entry.getValue() == null || entry.getValue().length == 0) {
                continue;
            }
			
            key = key.trim().toLowerCase(Locale.ENGLISH);
            if(key.equals("content-md5")) {
                contentMd5 = entry.getValue()[0];
                continue;
            }
			
            if(key.equals("content-type")) {
                contentType = entry.getValue()[0];
                continue;
            }
			
            if(key.equals("date")) {
                date = entry.getValue()[0];
                continue;
            }
			
            if(key.startsWith(OBS_PREFIX)) {				
                for(String value : entry.getValue()) {
                    if(value != null) {
                        temp.add(value.trim());
                    }
                }
                canonicalizedHeaders.put(key, this.join(temp, ","));
                temp.clear();
            }
        }
	// If the header contains x-obs-date, leave the Date header blank.
        if(canonicalizedHeaders.containsKey("x-obs-date")) {
            date = "";
        }	
		
        // Construct the StringToSign by concatenating HTTP-Verb, Content-MD5, Content-Type, and Date.
        StringBuilder stringToSign = new StringBuilder();
        stringToSign.append(httpMethod).append(SIGN_SEP)
            .append(contentMd5).append(SIGN_SEP)
            .append(contentType).append(SIGN_SEP)
            .append(date).append(SIGN_SEP);
			
        // Construct the StringToSign by concatenating CanonicalizedHeaders.
        for(Map.Entry<String, String> entry : canonicalizedHeaders.entrySet()) {
            stringToSign.append(entry.getKey()).append(":").append(entry.getValue()).append(SIGN_SEP);
        }
		
        // Construct the StringToSign by concatenating CanonicalizedResource.
        stringToSign.append("/");
        if(this.isValid(bucketName)) {
            stringToSign.append(bucketName).append("/");
            if(this.isValid(objectName)) {
                stringToSign.append(this.urlEncode(objectName));
            }
        }
		
        TreeMap<String, String> canonicalizedResource = new TreeMap<String, String>();
        for(Map.Entry<String, String> entry : queries.entrySet()) {
            key = entry.getKey();
            if(key == null) {
                continue;
            }
			
            if(SUB_RESOURCES.contains(key)) {
                canonicalizedResource.put(key, entry.getValue());
            }
        }
		
        if(canonicalizedResource.size() > 0) {
            stringToSign.append("?");
            for(Map.Entry<String, String> entry : canonicalizedResource.entrySet()) {
                stringToSign.append(entry.getKey());
                if(this.isValid(entry.getValue())) {
                    stringToSign.append("=").append(entry.getValue());
                }
                stringToSign.append("&");
            }
            stringToSign.deleteCharAt(stringToSign.length()-1);
        }
		
        // System.out.println(String.format("StringToSign:%s%s", SIGN_SEP, stringToSign.toString()));
		
        return stringToSign.toString();
    }
	
    public String headerSignature(String httpMethod, Map<String, String[]> headers, Map<String, String> queries, String bucketName, String objectName) throws Exception {

    // Construct a StringToSign.
        String stringToSign = this.stringToSign(httpMethod, headers, queries, bucketName, objectName);
		
        // Calculate the signature.
        return String.format("OBS %s:%s", this.ak, this.hmacSha1(stringToSign));
    }
	
    public static void main(String[] args) throws Exception {
        SignDemo demo = new SignDemo();

        /* Hard-coded or plaintext AK and SK are risky. For security purposes, encrypt your AK and SK and store them in the configuration file or environment variables.
        In this example, the AK and SK are stored in environment variables for identity authentication. Before running the code in this example, configure environment variables HUAWEICLOUD_SDK_AK and HUAWEICLOUD_SDK_SK. */
        demo.ak = System.getenv("HUAWEICLOUD_SDK_AK");
        demo.sk = System.getenv("HUAWEICLOUD_SDK_SK");
		
        String bucketName = "bucket-test";
        String objectName = "hello.jpg";
        Map<String, String[]> headers = new HashMap<String, String[]>();
        headers.put("date", new String[] {"Sat, 12 Oct 2015 08:12:38 GMT"});
        headers.put("x-obs-acl", new String[] {"public-read"});
        headers.put("x-obs-meta-key1", new String[] {"value1"});
        headers.put("x-obs-meta-key2", new String[] {"value2", "value3"});
        Map<String, String> queries = new HashMap<String, String>();
        queries.put("acl", null);
	
        //Calculate and print the signature carried in the header.
        System.out.println(demo.headerSignature("PUT", headers, queries, bucketName, objectName));
    }
	
}

Python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import os
import sys
import hashlib
import hmac
import binascii
from datetime import datetime
IS_PYTHON2 = sys.version_info.major == 2 or sys.version < '3'

# Hard-coded or plaintext AK and SK are risky. For security purposes, encrypt your AK and SK and store them in the configuration file or environment variables.
# In this example, the AK and SK are stored in environment variables for identity authentication. Before running the code in this example, configure environment variables HUAWEICLOUD_SDK_AK and HUAWEICLOUD_SDK_SK.
yourSecretAccessKeyID = os.getenv('HUAWEICLOUD_SDK_SK')

httpMethod = "PUT"
contentType = "application/xml"
# "date" indicates when the request was generated.
date = datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
canonicalizedHeaders = "x-obs-acl:private\n"
CanonicalizedResource = "/newbucketname2"

# Construct a StringToSign.
canonical_string = httpMethod + "\n" + "\n" + contentType + "\n" + date + "\n" + canonicalizedHeaders + CanonicalizedResource

# Calculate the signature using "Signature = Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) )"
if IS_PYTHON2:    
    hashed = hmac.new(yourSecretAccessKeyID, canonical_string, hashlib.sha1)    
    encode_canonical = binascii.b2a_base64(hashed.digest())[:-1]
else:    
    hashed = hmac.new(yourSecretAccessKeyID.encode('UTF-8'), canonical_string.encode('UTF-8'), hashlib.sha1)
    encode_canonical = binascii.b2a_base64(hashed.digest())[:-1].decode('UTF-8')

print(encode_canonical)

C

Download the sample code for calculating a signature with the C language.

  1. The API for calculating the signature is included in the sign.h header file.
  2. The example code for calculating the signature is included in the main.c file.

Addressing a Signature Mismatch

During an OBS API call, if the following error is reported:

Status code: 403 Forbidden

Error code: SignatureDoesNotMatch

Error message: The request signature we calculated does not match the signature you provided. Check your key and signing method.

Address the problem by referring to Why Don't the Signatures Match?