인증 관리

S3 API를 사용하여 네이버 클라우드 플랫폼 Object Storage에서 생성되는 요청은 AWS의 authorization 헤더 구현체를 사용하여 인증되어야 합니다. 네이버 클라우드 플랫폼 Object Storage는 Signature Version 2와 Signature Version 4 인증 방식을 지원합니다. Signature Version 4 인증 방식은 서명 인증 단계에서 접근용 시크릿 키가 아닌 파생된 키를 사용하고 있어 좀 더 안전합니다. 또한 요청에 포함된 타임스탬프와 해당 서명이 연결되어 있어 인증 헤더를 재사용할 수 없기 때문에 전송되는 데이터의 무결성을 보장받을 수 있습니다. 헤더는 ‘알고리즘’, ‘Credential 정보(scope)’, ‘서명된 헤더 목록’, ‘서명’의 네 가지 요소로 구성되어 있습니다.

AWS4-HMAC-SHA256 Credential={access-key}/{date}/{region}/s3/aws4_request,SignedHeaders=host;x-amz-date;{other-required-headers},Signature={signature}

날짜는 YYYYMMDD 형식으로 표시되고, 리전은 네이버 클라우드 플랫폼 Object Storage API 소개 > 호출 도메인(Endpoint)에 따라 입력합니다. host 및 x-amz-date 헤더는 항상 필수이며, 요청에 따라 다른 헤더가 필요할 수 있습니다(예: 페이로드를 포함한 요청인 경우 x-amz-content-sha256 필요). 개별 요청마다 서명을 다시 계산해야 하기 때문에 대부분의 개발자가 인증 헤더를 자동으로 생성하는 도구나 SDK를 사용하는 것을 권장합니다. authorization 헤더를 생성하기 전에 먼저 표준화 형식으로 요청을 작성해야 합니다.

  1. 사용하려는 HTTP 메서드를 선언합니다(예: PUT).
  2. 접근하려는 리소스를 표준화된 방식으로 정의합니다. 이는 http(s)://와 쿼리 문자열 사이의 주소를 작성하는 단계입니다. 계정 레벨의 요청(예: 버킷 조회)인 경우 주소 다음에 / 문자만 추가하면 됩니다.
  3. 요청 파라미터가 있는 경우에는 표준 방식으로 URL을 인코딩(예: 공백 자리에 % 문자를 사용해 %20)과 같이 표시)하고 알파벳순으로 나열합니다.
  4. 헤더에서는 공백을 제거하고, 대문자는 소문자로 변환하고, 각 헤더 항목의 끝에 개행 문자를 삽입합니다. 이때 헤더 목록은 ASCII순으로 정렬되어야 합니다.
  5. 표준 형식으로 나열된 헤더에 서명을 추가합니다. 서명에는 값이 아닌 헤더 이름이 포함됩니다. 알파벳순으로 나열하고 세미콜론으로 구분한 헤더 이름을 나열합니다. 모든 요청에는 반드시 host 및 x-amz-date가 포함되어야 합니다.
  6. 오브젝트를 업로드하거나 ACL을 생성하는 요청에 본문이 있는 경우 요청 본문은 SHA-256 알고리즘을 사용하여 해시되어야 하고 Base16으로 인코딩된 소문자로 표시되어야 합니다.
  7. HTTP 메서드, 표준 리소스, 표준 파라미터, 표준 헤더, 서명된 헤더, 해시된 요청 본문을 결합합니다. 이때 각 항목의 끝에는 개행 문자를 삽입합니다.

다음에서는 "서명할 문자열"을 만들고 이를 서명 키와 결합하여 서명을 확정하는 방법을 설명합니다. "서명할 문자열"은 다음과 같은 형식으로 구성됩니다.

AWS4-HMAC-SHA256
{time}
{date}/{string}/s3/aws4_request
{hashed-standardized-request}
  1. 시간은 UTC(Coordinated Universal Time)를 기준으로 ISO 8601 표준 규정에 따라 표시되어야 합니다(예: 20161128T152924Z).
  2. 날짜 형식은 YYYYMMDD입니다.
  3. 마지막 줄은 SHA-256 알고리즘을 사용하여 직전에 생성한 표준 요청 해시를 나타냅니다.

다음에서는 서명을 계산하는 방법을 설명합니다.

  1. 먼저 해당 계정의 시크릿 키, 현재 날짜, 리전, 사용 중인 API 타입 정보를 기반으로 생성된 서명 키가 있어야 합니다.
  2. AWS4 문자열이 시크릿 키 앞에 추가됩니다. 해당 문자열은 날짜를 해시하는 키로 사용됩니다.
  3. 생성된 해시값은 리전을 해시하는 키로 사용됩니다.
  4. 이와 같은 프로세스는 API 타입을 해시하기 위한 키로 사용될 해시값이 생성될 때까지 계속됩니다.
  5. 마지막에 생성된 해시값은 서명 키를 생성하는 aws4_request 문자열을 해시하는 키로 사용됩니다.
  6. 생성된 서명 키는 최종 서명을 생성할 ‘서명할 문자열’을 해시하는 키로 사용됩니다.

마지막 단계는 아래 예와 같이 각 정보를 조합하여 authorization 헤더를 생성하는 것입니다.

AWS4-HMAC-SHA256 Credential={access-key}/{date}/{region}/s3/aws4_request,SignedHeaders=host;x-amz-date;{other-required-headers},Signature={signature}

authorization 헤더 생성 예 (Java)

public class ObjectStorageSample {
    private static byte[] sign(String stringData, byte[] key) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException {
        byte[] data = stringData.getBytes(CHARSET_NAME);
        Mac e = Mac.getInstance(HMAC_ALGORITHM);
        e.init(new SecretKeySpec(key, HMAC_ALGORITHM));
        return e.doFinal(data);
    }

    private static String hash(String text) throws NoSuchAlgorithmException, UnsupportedEncodingException {
        MessageDigest e = MessageDigest.getInstance(HASH_ALGORITHM);
        e.update(text.getBytes(CHARSET_NAME));
        return Hex.encodeHexString(e.digest());
    }

    private static String getStandardizedQueryParameters(String queryString) throws UnsupportedEncodingException {
        TreeMap<String, String> sortedQueryParameters = new TreeMap<>();
        // sort by key name
        if (queryString != null && !queryString.isEmpty()) {
            String[] queryStringTokens = queryString.split("&");
            for (String field : queryStringTokens) {
                String[] fieldTokens = field.split("=");
                if (fieldTokens.length > 0) {
                    if (fieldTokens.length > 1) {
                        sortedQueryParameters.put(fieldTokens[0], fieldTokens[1]);
                    } else {
                        sortedQueryParameters.put(fieldTokens[0], "");
                    }
                }
            }
        }

        StringBuilder standardizedQueryParametersBuilder = new StringBuilder();
        int count = 0;
        for (String key : sortedQueryParameters.keySet()) {
            if (count > 0) {
                standardizedQueryParametersBuilder.append("&");
            }
            standardizedQueryParametersBuilder.append(key).append("=");

            if (sortedQueryParameters.get(key) != null && !sortedQueryParameters.get(key).isEmpty()) {
                standardizedQueryParametersBuilder.append(URLEncoder.encode(sortedQueryParameters.get(key), CHARSET_NAME));
            }

            count++;
        }
        return standardizedQueryParametersBuilder.toString();
    }

    private static TreeMap<String, String> getSortedHeaders(Header[] headers) {
        TreeMap<String, String> sortedHeaders = new TreeMap<>();
        // sort by header name
        for (Header header : headers) {
            sortedHeaders.put(header.getName(), header.getValue());
        }

        return sortedHeaders;
    }

    private static String getSignedHeaders(TreeMap<String, String> sortedHeaders) {
        StringBuilder signedHeadersBuilder = new StringBuilder();
        for (String headerName : sortedHeaders.keySet()) {
            signedHeadersBuilder.append(headerName.toLowerCase()).append(";");
        }
        return signedHeadersBuilder.toString();
    }

    private static String getStandardizedHeaders(TreeMap<String, String> sortedHeaders) {
        StringBuilder standardizedHeadersBuilder = new StringBuilder();
        for (String headerName : sortedHeaders.keySet()) {
            standardizedHeadersBuilder.append(headerName.toLowerCase()).append(":").append(sortedHeaders.get(headerName)).append("\n");
        }

        return standardizedHeadersBuilder.toString();
    }

    private static String getCanonicalRequest(HttpUriRequest request, String standardizedQueryParameters, String standardizedHeaders, String signedHeaders) {
        StringBuilder canonicalRequestBuilder = new StringBuilder().append(request.getMethod()).append("\n")
            .append(request.getURI().getPath()).append("\n")
            .append(standardizedQueryParameters).append("\n")
            .append(standardizedHeaders).append("\n")
            .append(signedHeaders).append("\n")
            .append(UNSIGNED_PAYLOAD);

        return canonicalRequestBuilder.toString();
    }

    private static String getScope(String datestamp, String regionName) {
        StringBuilder scopeBuilder = new StringBuilder().append(datestamp).append("/")
            .append(regionName).append("/")
            .append(SERVICE_NAME).append("/")
            .append(REQUEST_TYPE);
        return scopeBuilder.toString();
    }

    private static String getStringToSign(String timestamp, String scope, String canonicalRequest) throws NoSuchAlgorithmException, UnsupportedEncodingException {
        StringBuilder stringToSignBuilder = new StringBuilder(AWS_ALGORITHM)
            .append("\n")
            .append(timestamp).append("\n")
            .append(scope).append("\n")
            .append(hash(canonicalRequest));

        return stringToSignBuilder.toString();
    }

    private static String getSignature(String secretKey, String datestamp, String regionName, String stringToSign) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException {
        byte[] kSecret = ("AWS4" + secretKey).getBytes(CHARSET_NAME);
        byte[] kDate = sign(datestamp, kSecret);
        byte[] kRegion = sign(regionName, kDate);
        byte[] kService = sign(SERVICE_NAME, kRegion);
        byte[] signingKey = sign(REQUEST_TYPE, kService);

        return Hex.encodeHexString(sign(stringToSign, signingKey));
    }

    private static String getAuthorization(String accessKey, String scope, String signedHeaders, String signature) {
        String signingCredentials = accessKey + "/" + scope;
        String credential = "Credential=" + signingCredentials;
        String signerHeaders = "SignedHeaders=" + signedHeaders;
        String signatureHeader = "Signature=" + signature;

        StringBuilder authHeaderBuilder = new StringBuilder().append(AWS_ALGORITHM).append(" ")
            .append(credential).append(", ")
            .append(signerHeaders).append(", ")
            .append(signatureHeader);

        return authHeaderBuilder.toString();
    }

    private static void authorization(HttpUriRequest request, String regionName, String accessKey, String secretKey) throws Exception {
        Date now = new Date();
        DATE_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC"));
        TIME_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC"));
        String datestamp = DATE_FORMATTER.format(now);
        String timestamp = TIME_FORMATTER.format(now);

        request.addHeader("X-Amz-Date", timestamp);

        request.addHeader("X-Amz-Content-Sha256", UNSIGNED_PAYLOAD);

        String standardizedQueryParameters = getStandardizedQueryParameters(request.getURI().getQuery());

        TreeMap<String, String> sortedHeaders = getSortedHeaders(request.getAllHeaders());
        String signedHeaders = getSignedHeaders(sortedHeaders);
        String standardizedHeaders = getStandardizedHeaders(sortedHeaders);

        String canonicalRequest = getCanonicalRequest(request, standardizedQueryParameters, standardizedHeaders, signedHeaders);
        System.out.println("> canonicalRequest :");
        System.out.println(canonicalRequest);

        String scope = getScope(datestamp, regionName);

        String stringToSign = getStringToSign(timestamp, scope, canonicalRequest);
        System.out.println("> stringToSign :");
        System.out.println(stringToSign);

        String signature = getSignature(secretKey, datestamp, regionName, stringToSign);

        String authorization = getAuthorization(accessKey, scope, signedHeaders, signature);
        request.addHeader("Authorization", authorization);
    }

    private static void putObject(String bucketName, String objectName, String localFilePath) throws Exception {
        HttpClient httpClient = HttpClientBuilder.create().build();

        HttpPut request = new HttpPut(ENDPOINT + "/" + bucketName + "/" + objectName);
        request.addHeader("Host", request.getURI().getHost());
        request.setEntity(new FileEntity(new File(localFilePath)));

        authorization(request, REGION_NAME, ACCESS_KEY, SECRET_KEY);

        HttpResponse response = httpClient.execute(request);
        System.out.println("Response : " + response.getStatusLine());
    }

    private static void getObject(String bucketName, String objectName, String localFilePath) throws Exception {
        HttpClient httpClient = HttpClientBuilder.create().build();
        HttpGet request = new HttpGet(ENDPOINT + "/" + bucketName + "/" + objectName);
        request.addHeader("Host", request.getURI().getHost());

        authorization(request, REGION_NAME, ACCESS_KEY, SECRET_KEY);

        HttpResponse response = httpClient.execute(request);
        System.out.println("Response : " + response.getStatusLine());

        InputStream is = response.getEntity().getContent();
        File targetFile = new File(localFilePath);
        OutputStream os = new FileOutputStream(targetFile);

        byte[] buffer = new byte[8 * 1024];
        int bytesRead;
        while ((bytesRead = is.read(buffer)) != -1) {
            os.write(buffer, 0, bytesRead);
        }

        is.close();
        os.close();
    }

    private static void listObjects(String bucketName, String queryString) throws Exception {
        HttpClient httpClient = HttpClientBuilder.create().build();
        URI uri = new URI(ENDPOINT + "/" + bucketName + "?" + queryString);
        HttpGet request = new HttpGet(uri);
        request.addHeader("Host", request.getURI().getHost());

        authorization(request, REGION_NAME, ACCESS_KEY, SECRET_KEY);

        HttpResponse response = httpClient.execute(request);
        System.out.println("> Response : " + response.getStatusLine());
        int i;
        InputStream is = response.getEntity().getContent();
        StringBuffer buffer = new StringBuffer();
        byte[] b = new byte[4096];
        while ((i = is.read(b)) != -1) {
            buffer.append(new String(b, 0, i));
        }
        System.out.println(buffer.toString());

    }

    private static final String CHARSET_NAME = "UTF-8";
    private static final String HMAC_ALGORITHM = "HmacSHA256";
    private static final String HASH_ALGORITHM = "SHA-256";
    private static final String AWS_ALGORITHM = "AWS4-HMAC-SHA256";

    private static final String SERVICE_NAME = "s3";
    private static final String REQUEST_TYPE = "aws4_request";

    private static final String UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";

    private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyyMMdd");
    private static final SimpleDateFormat TIME_FORMATTER = new SimpleDateFormat("yyyyMMdd\'T\'HHmmss\'Z\'");

    private static final String REGION_NAME = "kr-standard";
    private static final String ENDPOINT = "https://kr.object.ncloudstorage.com";
    private static final String ACCESS_KEY = "ACCESS_KEY_ID";
    private static final String SECRET_KEY = "SECRET_KEY";

    public static void main(String[] args) throws Exception {
        String bucketName = "sample-bucket";
        String objectName = "sample-object.txt";
        String sourceFilePath = "/tmp/source.txt";
        String targetFilePath = "/tmp/target.txt";

        putObject(bucketName, objectName, sourceFilePath);

        getObject(bucketName, objectName, targetFilePath);

        String queryString = "max-keys=10&delimiter=/";
        listObjects(bucketName, queryString);
    }
}

authorization 헤더 생성 예 (Python2.7)

import hashlib
import hmac
import datetime
import requests
import urllib


def get_hash(key, msg):
    return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()


def create_signed_headers(headers):
    signed_headers = []

    for k in sorted(headers):
        signed_headers.append('%s;' % k)

    return ''.join(signed_headers)


def create_standardized_headers(headers):
    signed_headers = []

    for k in sorted(headers):
        signed_headers.append('%s:%s\n' % (k, headers[k]))

    return ''.join(signed_headers)


def create_standardized_query_parameters(request_parameters):
    standardized_query_parameters = []

    if request_parameters:
        for k in sorted(request_parameters):
            standardized_query_parameters.append('%s=%s' % (k, urllib.quote(request_parameters[k], safe='')))

        return '&'.join(standardized_query_parameters)
    else:
        return ''


class ObjectStorageSample:
    def __init__(self):
        self.region = 'kr-standard'
        self.endpoint = 'https://kr.object.ncloudstorage.com'
        self.host = 'kr.object.ncloudstorage.com'
        self.access_key = 'ACCESS_KEY_ID'
        self.secret_key = 'SECRET_KEY'

        self.payload_hash = 'UNSIGNED-PAYLOAD'
        self.hashing_algorithm = 'AWS4-HMAC-SHA256'
        self.service_name = 's3'
        self.request_type = 'aws4_request'

        self.time_format = '%Y%m%dT%H%M%SZ'
        self.date_format = '%Y%m%d'

    def _create_credential_scope(self, date_stamp):
        return date_stamp + '/' + self.region + '/' + self.service_name + '/' + self.request_type

    def _create_canonical_request(self, http_method, request_path, request_parameters, headers):
        standardized_query_parameters = create_standardized_query_parameters(request_parameters)
        standardized_headers = create_standardized_headers(headers)
        signed_headers = create_signed_headers(headers)

        canonical_request = (http_method + '\n' +
                             request_path + '\n' +
                             standardized_query_parameters + '\n' +
                             standardized_headers + '\n' +
                             signed_headers + '\n' +
                             self.payload_hash)

        print('canonical_request:\n%s\n' % canonical_request)
        return canonical_request

    def _create_string_to_sign(self, time_stamp, credential_scope, canonical_request):
        string_to_sign = (self.hashing_algorithm + '\n' +
                          time_stamp + '\n' +
                          credential_scope + '\n' +
                          hashlib.sha256(canonical_request.encode('utf-8')).hexdigest())

        print('string_to_sign:\n%s\n' % string_to_sign)
        return string_to_sign

    def _create_signature_key(self, date_stamp):
        key_date = get_hash(('AWS4' + self.secret_key).encode('utf-8'), date_stamp)
        key_string = get_hash(key_date, self.region)
        key_service = get_hash(key_string, self.service_name)
        key_signing = get_hash(key_service, self.request_type)
        return key_signing

    def _create_authorization_header(self, headers, signature_key, string_to_sign, credential_scope):
        signed_headers = create_signed_headers(headers)
        signature = hmac.new(signature_key, string_to_sign.encode('utf-8'), hashlib.sha256).hexdigest()

        return (self.hashing_algorithm + ' ' +
                'Credential=' + self.access_key + '/' + credential_scope + ', ' +
                'SignedHeaders=' + signed_headers + ', ' +
                'Signature=' + signature)

    def _sign(self, http_method, request_path, headers, time, request_parameters=None):
        time_stamp = time.strftime(self.time_format)
        date_stamp = time.strftime(self.date_format)

        credential_scope = self._create_credential_scope(date_stamp)
        canonical_request = self._create_canonical_request(http_method, request_path, request_parameters, headers)
        string_to_sign = self._create_string_to_sign(time_stamp, credential_scope, canonical_request)
        signature_key = self._create_signature_key(date_stamp)

        headers['authorization'] = self._create_authorization_header(headers, signature_key, string_to_sign, credential_scope)

    def put_object(self, bucket_name, object_name, source_file_path, request_parameters=None):
        http_method = 'PUT'

        with open(source_file_path) as f:
            time = datetime.datetime.utcnow()
            time_stamp = time.strftime(self.time_format)

            headers = {'x-amz-date': time_stamp,
                       'x-amz-content-sha256': self.payload_hash,
                       'host': self.host}

            request_path = '/%s/%s' % (bucket_name, object_name)

            self._sign(http_method, request_path, headers, time, request_parameters)

            request_url = self.endpoint + request_path
            r = requests.put(request_url, headers=headers, params=request_parameters, data=f.read())

            print('Response code: %d' % r.status_code)

    def get_object(self, bucket_name, object_name, target_file_path, request_parameters=None):
        http_method = 'GET'

        time = datetime.datetime.utcnow()
        time_stamp = time.strftime(self.time_format)

        headers = {'x-amz-date': time_stamp,
                   'x-amz-content-sha256': self.payload_hash,
                   'host': self.host}

        request_path = '/%s/%s' % (bucket_name, object_name)

        self._sign(http_method, request_path, headers, time, request_parameters)

        request_url = self.endpoint + request_path
        r = requests.get(request_url, headers=headers, params=request_parameters, stream=True)

        print('Response code: %d' % r.status_code)

        if r.status_code == 200:
            with open(target_file_path, 'wb') as f:
                f.write(r.content)

    def list_objects(self, bucket_name, request_parameters=None):
        http_method = 'GET'

        time = datetime.datetime.utcnow()
        time_stamp = time.strftime(self.time_format)

        headers = {'x-amz-date': time_stamp,
                   'x-amz-content-sha256': self.payload_hash,
                   'host': self.host}

        request_path = '/%s' % bucket_name

        self._sign(http_method, request_path, headers, time, request_parameters)

        request_url = self.endpoint + request_path
        r = requests.get(request_url, headers=headers, params=request_parameters)

        print('Response code: %d' % r.status_code)
        print('Response content:\n%s' % r.content)


if __name__ == '__main__':
    sample = ObjectStorageSample()
    sample.put_object('sample-bucket', 'sample-object.txt', '/tmp/source.txt')
    sample.get_object('sample-bucket', 'sample-object.txt', '/tmp/target.txt')
    sample.list_objects('sample-bucket', request_parameters={'max-keys': '10', 'delimiter': '/'})

""에 대한 건이 검색되었습니다.

    ""에 대한 검색 결과가 없습니다.

    처리중...