※ 2020/7/27 수정 사항 - 업로드 메소드가 GET 또는 POST 중 하나를 사용하도록 표기되어 있었는데, 이 중 POST가 아닌 PUT으로 입력해야 업로드 작업이 정상적으로 수행됨

(AWS SIgnature v4 Shell Script 버전은 다음을 참조)

 

AWS에서 제공하는 기능 중 하나인 Signature는 명시된 프로세스에 따라 Request URL을 생성하면 AWS 서비스에 대해 http 방식으로 요청을 할 수 있도록 해준다.

(정확히는 URL에 암호화된 Credential 키를 추가하여 AWS에서 검증하는 과정이라 할 수 있다.)

개인적인 필요로 인해 S3 버킷에있는 프라이빗 객체를 업로드/다운로드해야 할 일이 생겼는데, AWS 툴킷(cli, sdk 등)을 사용하지 않고 수행해야 하는 상황에 따라 위의 시그니처 방식을 사용하게 되었다.

처음에는 다른 사용자도 별도로 환경을 구축하지 않고 사용이 필요하여 파워쉘 스크립트를 찾아다녔는데, AWS에서 공개된 샘플은 파이썬 방식이거나 구글링해서 나오는 파워쉘 스크립트의 시그니처 생성 알고리즘이 구버전으로 되어있어 작동하지 않아 결국 직접 구현하게 되었다(...) 사용하기에 앞서 credential에 s3 읽기/쓰기 권한 부여가 필요하다.

현재 AWS Signature 버전은 4이며, AWS4-HMAC-SHA256라는 알고리즘을 사용한다. 다음 버전이 나올경우 해당 스크립트도 사용 불가능할 것으로 예상되니 사용 전에 확인하도록 한다.

<#########################
FUNCTIONS
#########################>

function getSHA256($data) {

    $hash = [System.Security.Cryptography.SHA256]::Create()
    $array = $hash.ComputeHash( [System.Text.Encoding]::UTF8.GetBytes($data) )
    return $array

}

function HmacSHA256($data, $key) {

    $hmacsha = New-Object System.Security.Cryptography.HMACSHA256    
    $hmacsha.key = $key    
    $sign = $hmacsha.ComputeHash([Text.Encoding]::UTF8.GetBytes($data))
    return $sign

}

function getStringFromByte($byteArray) {

    $stringBuilder = ""
    $byteArray | ForEach { $stringBuilder += $_.ToString("x2") }
    return $stringBuilder

}

function getSignatureKey([String] $key, [String] $dateStamp, [String] $regionName, [String] $serviceName) {

    $kSecret = [Text.Encoding]::UTF8.GetBytes("AWS4$($key)")
    $kDate = HmacSHA256 -data $dateStamp -key $kSecret
    $kRegion = HmacSHA256 -data $regionName -key $kDate
    $kService = HmacSHA256 -data $serviceName -key $kRegion
    $kSigning = HmacSHA256 -data "aws4_request" -key $kService

    return $kSigning # return of signing key as byte array

}

<#########################
PREPARATION
#########################>

$bucket = "s3-mybucket" # 버킷
$s3key = "mydir/myobj" # 객체
$service = "s3" 
$region = "ap-northeast-2" # 리전
$accessKey = "" # access key 입력
$secretKey = "" # secret key 입력
$expires = 60 # 유효시간(분)

[System.Uri]$endpoint = "https://$($bucket).$($service).$($region).amazonaws.com"

$currentDate = Get-Date
$date = $currentDate.ToUniversalTime().ToString("yyyyMMddTHHmmssZ")
$dateStamp = $currentDate.ToUniversalTime().ToString("yyyyMMdd")
$scope = "$($dateStamp)/$($region)/$($service)/aws4_request"

$AllProtocols = [System.Net.SecurityProtocolType]'Tls11,Tls12'
[System.Net.ServicePointManager]::SecurityProtocol = $AllProtocols

#[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$false}

[System.Uri]$endpoint = "$($endpoint.AbsoluteUri)$($s3key)"

<#########################
CREATE THE CANONICAL REQUEST
#########################>

Add-Type -AssemblyName System.Web

$algorithm = "AWS4-HMAC-SHA256"
$credentialUrl = [System.Web.HttpUtility]::UrlEncode("$($accessKey)/$($scope)").replace("%2f", "%2F")
$canonicalHeaders="host:$($endpoint.Host)`n"
$signedHeaders="host"
$payloadHash="UNSIGNED-PAYLOAD"

$canonicalQueryString = "X-Amz-Algorithm=$($algorithm)"
$canonicalQueryString += "&X-Amz-Credential=$($credentialUrl)"
$canonicalQueryString += "&X-Amz-Date=$($date)"
$canonicalQueryString += "&X-Amz-Expires=$($expires*60)"
$canonicalQueryString += "&X-Amz-SignedHeaders=$($signedHeaders)"

$canonicalRequestPlain = "$($verb)`n$( $endpoint.AbsolutePath )`n$($canonicalQueryString)`n$($canonicalHeaders)`n$($signedHeaders)`n$($payloadHash)"
$canonicalRequestByte = getSHA256 -data $canonicalRequestPlain
$canonicalRequestHash = getStringFromByte -byteArray $canonicalRequestByte

<#########################
CREATE THE STRING TO SIGN
#########################>

$stringToSign = "$($algorithm)`n$($date)`n$($scope)`n$( $canonicalRequestHash )"

<#########################
CREATE THE SIGNATURE KEY
#########################>

$signature = getSignatureKey -key $secretKey -dateStamp $dateStamp -regionName $region -serviceName $service

<#########################
CREATE THE SIGNATURE
#########################>

$signatureByte = HmacSHA256 -data $stringToSign -key $signature
$signatureHash = getStringFromByte -byteArray $signatureByte

$canonicalQueryString += "&X-Amz-Signature=$($signatureHash)"

$requestUrl = "$($endpoint.AbsoluteUri)?$($canonicalQueryString)"

<#########################
# CREATE COMMANDLINE
#########################>

$os = "Windows" # (or "Linux")
$verb = "GET" # (or "PUT")
$dir = "C:\" # (or "~" 등)

if($os.StartsWith("W")){
    $commandLine = "Invoke-WebRequest -Method $($verb) $( if ($verb -eq "GET") { "-OutFile" } else { "-InFile" } ) $($dir)\$($s3key.Split('/')[-1]) -Uri ""$($requestUrl)"""
    Write-Output $commandLine
}
else {
    $commandLine = "curl $( if ($verb -eq "GET") { "-o" } else { "-T" } ) ""$($dir)/$($s3key.Split('/')[-1])"" ""$($requestUrl)"""
    Write-Output $commandLine
}

이후 PREPARATION 및 CREATE COMMANDLINE 파트의 변수를 사용자 환경에 맞게 구성하며 사용하면 된다.

(PowerShell 버전3 이상에서 호환되도록 구성했으며 Windows Server 2012 이상에서 실행되는 것을 확인했다.)

 

- GET Method 입력 시 $dir 경로로 $s3key 파일을 다운로드, PUT Method 입력 시 $dir 경로의 $s3key 파일을 다운로드 하는 url이 생성된다.

- 실행 시 지정 경로에 대해 파일을 다운받거나 업로드할 수 있도록 os타입에 따라 간편히 명령어를 생성해주는데, Request URL만 얻고 싶다면 단순히 CREATE THE SIGNATURE 파트 부분 마지막 줄의 $requestUrl 만 출력하도록 한다.

- 자동으로 실행까지 해주고 싶다면 마지막 줄에 Invoke-Expression $commandLine 을 추가한다.

2020/7/27 수정