Uwierzytelnianie HMAC — dokumentacja interfejsu API REST
Żądania HTTP można uwierzytelniać przy użyciu schematu uwierzytelniania HMAC-SHA256. (HMAC odnosi się do kodu uwierzytelniania komunikatów opartych na skrótach). Te żądania muszą być przesyłane za pośrednictwem protokołu TLS.
Wymagania wstępne
- Poświadczenia — <identyfikator klucza dostępu>
- Wpis tajny — zdekodowana wartość klucza dostępu base64.
base64_decode(<Access Key Value>)
Wartości poświadczeń (nazywane id
również ) i wpisu tajnego (nazywanego value
również ) muszą być uzyskane z wystąpienia aplikacja systemu Azure Configuration. Można to zrobić przy użyciu witryny Azure Portal lub interfejsu wiersza polecenia platformy Azure.
Podaj każde żądanie ze wszystkimi nagłówkami HTTP wymaganymi do uwierzytelniania. Minimalne wymagane są następujące elementy:
Nagłówek żądania | opis |
---|---|
Host | Host internetowy i numer portu. Aby uzyskać więcej informacji, zobacz sekcję 3.2.2. |
Data | Data i godzina, z której pochodzi żądanie. Nie może to być więcej niż 15 minut od bieżącego uniwersalnego czasu koordynowanego (Czas średni Greenwich). Wartość jest datą HTTP, zgodnie z opisem w sekcji 3.3.1. |
x-ms-date | Tak samo jak Date powyżej. Zamiast tego można go użyć, gdy agent nie może bezpośrednio uzyskać dostępu do nagłówka Date żądania lub zmodyfikować go przez serwer proxy. Date Jeśli x-ms-date obie te elementy x-ms-date mają pierwszeństwo. |
x-ms-content-sha256 | Algorytm SHA256 zakodowany w formacie base64 treści żądania. Należy go podać, nawet jeśli nie ma ciała. base64_encode(SHA256(body)) |
Autoryzacja | Informacje o uwierzytelnianiu wymagane przez schemat HMAC-SHA256. Format i szczegóły zostały wyjaśnione w dalszej części tego artykułu. |
Przykład:
Host: {myconfig}.azconfig.io
Date: Fri, 11 May 2018 18:48:36 GMT
x-ms-content-sha256: {SHA256 hash of the request body}
Authorization: HMAC-SHA256 Credential={Access Key ID}&SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature={Signature}
Nagłówek autoryzacji
Składnia
Authorization
: HMAC-SHA256 Credential
=<value&=value>&Signature
SignedHeaders
=<<value>>
Argument | opis |
---|---|
HMAC-SHA256 | Schemat autoryzacji. (wymagane) |
Poświadczeń | Identyfikator klucza dostępu używanego do obliczenia podpisu. (wymagane) |
SignedHeaders | Nagłówki żądań HTTP dodane do podpisu. (wymagane) |
Podpis | zakodowana w formacie base64 HMACSHA256 ciągu do znaku. (wymagane) |
Referencje
Identyfikator klucza dostępu używanego do obliczenia podpisu.
Podpisane nagłówki
Nazwy nagłówków żądania HTTP oddzielone średnikami wymagane do podpisania żądania. Te nagłówki HTTP muszą być również poprawnie dostarczone z żądaniem. Nie używaj białych spacji.
Wymagane nagłówki żądań HTTP
x-ms-date
[lub Date
];host
;x-ms-content-sha256
Do podpisywania można również dodać inne nagłówki żądań HTTP. Wystarczy dołączyć je do argumentu SignedHeaders
.
Przykład:
x-ms-date; gospodarz; x-ms-content-sha256;Content-Type
;Accept
Podpis
Zakodowana w formacie Base64 HMACSHA256 skrót ciągu do znaku. Używa on klucza dostępu określonego przez Credential
. base64_encode(HMACSHA256(String-To-Sign, Secret))
Ciąg do podpisania
Jest to kanoniczna reprezentacja żądania:
Ciąg do znaku =
HTTP_METHOD + '\n' + path_and_query + '\n' + signed_headers_values
Argument | opis |
---|---|
HTTP_METHOD | Wielkie litery nazwa metody HTTP używana z żądaniem. Aby uzyskać więcej informacji, zobacz sekcję 9. |
path_and_query | Łączenie ścieżki bezwzględnego identyfikatora URI żądania i ciągu zapytania. Aby uzyskać więcej informacji, zobacz sekcję 3.3. |
signed_headers_values | Rozdzielone średnikami wartości wszystkich nagłówków żądań HTTP wymienionych w pliku SignedHeaders . Format jest zgodny z SignedHeaders semantykami. |
Przykład:
string-To-Sign=
"GET" + '\n' + // VERB
"/kv?fields=*&api-version=1.0" + '\n' + // path_and_query
"Fri, 11 May 2018 18:48:36 GMT;{myconfig}.azconfig.io;{value of ms-content-sha256 header}" // signed_headers_values
Błędy
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256, Bearer
Przyczyna: Nie podano nagłówka żądania autoryzacji z schematem HMAC-SHA256.
Rozwiązanie: podaj prawidłowy Authorization
nagłówek żądania HTTP.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="The access token has expired", Bearer
Przyczyna: Date
lub x-ms-date
nagłówek żądania jest większy niż 15 minut od bieżącego uniwersalnego czasu koordynowanego (Czas średni Greenwich).
Rozwiązanie: podaj poprawną datę i godzinę.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Invalid access token date", Bearer
Przyczyna: Brak lub nieprawidłowy Date
nagłówek żądania.x-ms-date
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="[Credential][SignedHeaders][Signature] is required", Bearer
Przyczyna: Brak wymaganego parametru z nagłówka Authorization
żądania.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Invalid Credential", Bearer
Przyczyna: Podany identyfikator klucza dostępu [Host
]/[Access Key ID] nie został znaleziony.
Rozwiązanie: Sprawdź Credential
parametr nagłówka Authorization
żądania. Upewnij się, że jest to prawidłowy identyfikator klucza dostępu i upewnij się, że Host
nagłówek wskazuje zarejestrowane konto.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Invalid Signature", Bearer
Przyczyna: Podana Signature
zawartość nie jest zgodna z oczekiwaniami serwera.
Rozwiązanie: upewnij się, że String-To-Sign
parametr jest poprawny. Upewnij się, że element Secret
jest poprawny i prawidłowo używany (kodowanie base64 przed użyciem).
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Signed request header 'xxx' is not provided", Bearer
Przyczyna: Brak nagłówka żądania wymaganego przez SignedHeaders
parametr w nagłówku Authorization
.
Rozwiązanie: podaj wymagany nagłówek z poprawną wartością.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="XXX is required as a signed header", Bearer
Przyczyna: Brak parametru w pliku SignedHeaders
.
Rozwiązanie: Sprawdź minimalne wymagania dotyczące podpisanych nagłówków.
Fragmenty kodu
JavaScript
Wymagania wstępne: Crypto-JS
function signRequest(host,
method, // GET, PUT, POST, DELETE
url, // path+query
body, // request body (undefined of none)
credential, // access key id
secret) // access key value (base64 encoded)
{
var verb = method.toUpperCase();
var utcNow = new Date().toUTCString();
var contentHash = CryptoJS.SHA256(body).toString(CryptoJS.enc.Base64);
//
// SignedHeaders
var signedHeaders = "x-ms-date;host;x-ms-content-sha256"; // Semicolon separated header names
//
// String-To-Sign
var stringToSign =
verb + '\n' + // VERB
url + '\n' + // path_and_query
utcNow + ';' + host + ';' + contentHash; // Semicolon separated SignedHeaders values
//
// Signature
var signature = CryptoJS.HmacSHA256(stringToSign, CryptoJS.enc.Base64.parse(secret)).toString(CryptoJS.enc.Base64);
//
// Result request headers
return [
{ name: "x-ms-date", value: utcNow },
{ name: "x-ms-content-sha256", value: contentHash },
{ name: "Authorization", value: "HMAC-SHA256 Credential=" + credential + "&SignedHeaders=" + signedHeaders + "&Signature=" + signature }
];
}
C#
using (var client = new HttpClient())
{
var request = new HttpRequestMessage()
{
RequestUri = new Uri("https://{config store name}.azconfig.io/kv?api-version=1.0"),
Method = HttpMethod.Get
};
//
// Sign the request
request.Sign(<Credential>, <Secret>);
await client.SendAsync(request);
}
static class HttpRequestMessageExtensions
{
public static HttpRequestMessage Sign(this HttpRequestMessage request, string credential, string secret)
{
string host = request.RequestUri.Authority;
string verb = request.Method.ToString().ToUpper();
DateTimeOffset utcNow = DateTimeOffset.UtcNow;
string contentHash = Convert.ToBase64String(request.Content.ComputeSha256Hash());
//
// SignedHeaders
string signedHeaders = "date;host;x-ms-content-sha256"; // Semicolon separated header names
//
// String-To-Sign
var stringToSign = $"{verb}\n{request.RequestUri.PathAndQuery}\n{utcNow.ToString("r")};{host};{contentHash}";
//
// Signature
string signature;
using (var hmac = new HMACSHA256(Convert.FromBase64String(secret)))
{
signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.ASCII.GetBytes(stringToSign)));
}
//
// Add headers
request.Headers.Date = utcNow;
request.Headers.Add("x-ms-content-sha256", contentHash);
request.Headers.Authorization = new AuthenticationHeaderValue("HMAC-SHA256", $"Credential={credential}&SignedHeaders={signedHeaders}&Signature={signature}");
return request;
}
}
static class HttpContentExtensions
{
public static byte[] ComputeSha256Hash(this HttpContent content)
{
using (var stream = new MemoryStream())
{
if (content != null)
{
content.CopyToAsync(stream).Wait();
stream.Seek(0, SeekOrigin.Begin);
}
using (var alg = SHA256.Create())
{
return alg.ComputeHash(stream.ToArray());
}
}
}
}
Java
public CloseableHttpResponse signRequest(HttpUriRequest request, String credential, String secret)
throws IOException, URISyntaxException {
Map<String, String> authHeaders = generateHeader(request, credential, secret);
authHeaders.forEach(request::setHeader);
return httpClient.execute(request);
}
private static Map<String, String> generateHeader(HttpUriRequest request, String credential, String secret)
throws URISyntaxException, IOException {
String requestTime = GMT_DATE_FORMAT.format(new Date());
String contentHash = buildContentHash(request);
// SignedHeaders
String signedHeaders = "x-ms-date;host;x-ms-content-sha256";
// Signature
String methodName = request.getRequestLine().getMethod().toUpperCase();
URIBuilder uri = new URIBuilder(request.getRequestLine().getUri());
String scheme = uri.getScheme() + "://";
String requestPath = uri.toString().substring(scheme.length()).substring(uri.getHost().length());
String host = new URIBuilder(request.getRequestLine().getUri()).getHost();
String toSign = String.format("%s\n%s\n%s;%s;%s", methodName, requestPath, requestTime, host, contentHash);
byte[] decodedKey = Base64.getDecoder().decode(secret);
String signature = Base64.getEncoder().encodeToString(new HmacUtils(HMAC_SHA_256, decodedKey).hmac(toSign));
// Compose headers
Map<String, String> headers = new HashMap<>();
headers.put("x-ms-date", requestTime);
headers.put("x-ms-content-sha256", contentHash);
String authorization = String.format("HMAC-SHA256 Credential=%s, SignedHeaders=%s, Signature=%s",
credential, signedHeaders, signature);
headers.put("Authorization", authorization);
return headers;
}
private static String buildContentHash(HttpUriRequest request) throws IOException {
String content = "";
if (request instanceof HttpEntityEnclosingRequest) {
try {
StringWriter writer = new StringWriter();
IOUtils.copy(((HttpEntityEnclosingRequest) request).getEntity().getContent(), writer,
StandardCharsets.UTF_8);
content = writer.toString();
}
finally {
((HttpEntityEnclosingRequest) request).getEntity().getContent().close();
}
}
byte[] digest = new DigestUtils(SHA_256).digest(content);
return Base64.getEncoder().encodeToString(digest);
}
Golang
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"io/ioutil"
"net/http"
"strings"
"time"
)
//SignRequest Setup the auth header for accessing Azure App Configuration service
func SignRequest(id string, secret string, req *http.Request) error {
method := req.Method
host := req.URL.Host
pathAndQuery := req.URL.Path
if req.URL.RawQuery != "" {
pathAndQuery = pathAndQuery + "?" + req.URL.RawQuery
}
content, err := ioutil.ReadAll(req.Body)
if err != nil {
return err
}
req.Body = ioutil.NopCloser(bytes.NewBuffer(content))
key, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
return err
}
timestamp := time.Now().UTC().Format(http.TimeFormat)
contentHash := getContentHashBase64(content)
stringToSign := fmt.Sprintf("%s\n%s\n%s;%s;%s", strings.ToUpper(method), pathAndQuery, timestamp, host, contentHash)
signature := getHmac(stringToSign, key)
req.Header.Set("x-ms-content-sha256", contentHash)
req.Header.Set("x-ms-date", timestamp)
req.Header.Set("Authorization", "HMAC-SHA256 Credential="+id+", SignedHeaders=x-ms-date;host;x-ms-content-sha256, Signature="+signature)
return nil
}
func getContentHashBase64(content []byte) string {
hasher := sha256.New()
hasher.Write(content)
return base64.StdEncoding.EncodeToString(hasher.Sum(nil))
}
func getHmac(content string, key []byte) string {
hmac := hmac.New(sha256.New, key)
hmac.Write([]byte(content))
return base64.StdEncoding.EncodeToString(hmac.Sum(nil))
}
Python
import base64
import hashlib
import hmac
from datetime import datetime
import six
def sign_request(host,
method, # GET, PUT, POST, DELETE
url, # Path + Query
body, # Request body
credential, # Access Key ID
secret): # Access Key Value
verb = method.upper()
utc_now = str(datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S ")) + "GMT"
if six.PY2:
content_digest = hashlib.sha256(bytes(body)).digest()
else:
content_digest = hashlib.sha256(bytes(body, 'utf-8')).digest()
content_hash = base64.b64encode(content_digest).decode('utf-8')
# Signed Headers
signed_headers = "x-ms-date;host;x-ms-content-sha256" # Semicolon separated header names
# String-To-Sign
string_to_sign = verb + '\n' + \
url + '\n' + \
utc_now + ';' + host + ';' + content_hash # Semicolon separated SignedHeaders values
# Decode secret
if six.PY2:
decoded_secret = base64.b64decode(secret)
digest = hmac.new(decoded_secret, bytes(
string_to_sign), hashlib.sha256).digest()
else:
decoded_secret = base64.b64decode(secret, validate=True)
digest = hmac.new(decoded_secret, bytes(
string_to_sign, 'utf-8'), hashlib.sha256).digest()
# Signature
signature = base64.b64encode(digest).decode('utf-8')
# Result request headers
return {
"x-ms-date": utc_now,
"x-ms-content-sha256": content_hash,
"Authorization": "HMAC-SHA256 Credential=" + credential + "&SignedHeaders=" + signed_headers + "&Signature=" + signature
}
PowerShell
function Sign-Request(
[string] $hostname,
[string] $method, # GET, PUT, POST, DELETE
[string] $url, # path+query
[string] $body, # request body
[string] $credential, # access key id
[string] $secret # access key value (base64 encoded)
)
{
$verb = $method.ToUpperInvariant()
$utcNow = (Get-Date).ToUniversalTime().ToString("R", [Globalization.DateTimeFormatInfo]::InvariantInfo)
$contentHash = Compute-SHA256Hash $body
$signedHeaders = "x-ms-date;host;x-ms-content-sha256"; # Semicolon separated header names
$stringToSign = $verb + "`n" +
$url + "`n" +
$utcNow + ";" + $hostname + ";" + $contentHash # Semicolon separated signedHeaders values
$signature = Compute-HMACSHA256Hash $secret $stringToSign
# Return request headers
return @{
"x-ms-date" = $utcNow;
"x-ms-content-sha256" = $contentHash;
"Authorization" = "HMAC-SHA256 Credential=" + $credential + "&SignedHeaders=" + $signedHeaders + "&Signature=" + $signature
}
}
function Compute-SHA256Hash(
[string] $content
)
{
$sha256 = [System.Security.Cryptography.SHA256]::Create()
try {
return [Convert]::ToBase64String($sha256.ComputeHash([Text.Encoding]::ASCII.GetBytes($content)))
}
finally {
$sha256.Dispose()
}
}
function Compute-HMACSHA256Hash(
[string] $secret, # base64 encoded
[string] $content
)
{
$hmac = [System.Security.Cryptography.HMACSHA256]::new([Convert]::FromBase64String($secret))
try {
return [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::ASCII.GetBytes($content)))
}
finally {
$hmac.Dispose()
}
}
# Stop if any error occurs
$ErrorActionPreference = "Stop"
$uri = [System.Uri]::new("https://{myconfig}.azconfig.io/kv?api-version=1.0")
$method = "GET"
$body = $null
$credential = "<Credential>"
$secret = "<Secret>"
$headers = Sign-Request $uri.Authority $method $uri.PathAndQuery $body $credential $secret
Invoke-RestMethod -Uri $uri -Method $method -Headers $headers -Body $body
Bash
Wymagania wstępne:
Warunek wstępny | Polecenie | Przetestowane wersje |
---|---|---|
Bash | bash | 3.5.27, 4.4.23 |
coreutils | tr | 8.28 |
lok | lok | 7.55.1, 7.58.0 |
OpenSSL | openssl | 1.1.0g, 1.1.1a |
util-linux | hexdump | 2.14.1, 2.31.1 |
#!/bin/bash
sign_request () {
local host="$1"
local method="$2" # GET, PUT, POST, DELETE
local url="$3" # path+query
local body="$4" # request body
local credential="$5" # access key id
local secret="$6" # access key value (base64 encoded)
local verb=$(printf "$method" | tr '[:lower:]' '[:upper:]')
local utc_now="$(date -u '+%a, %d %b %Y %H:%M:%S GMT')"
local content_hash="$(printf "$body" | openssl sha256 -binary | base64)"
local signed_headers="x-ms-date;host;x-ms-content-sha256" # Semicolon separated header names
local string_to_sign="$verb\n$url\n$utc_now;$host;$content_hash" # Semicolon separated signed_headers values
local decoded_secret="$(printf "$secret" | base64 -d | hexdump -v -e '/1 "%02x"')"
local signature="$(printf "$string_to_sign" | openssl sha256 -mac HMAC -macopt hexkey:"$decoded_secret" -binary | base64)"
# Output request headers
printf '%s\n' \
"x-ms-date: $utc_now" \
"x-ms-content-sha256: $content_hash" \
"Authorization: HMAC-SHA256 Credential=$credential&SignedHeaders=$signed_headers&Signature=$signature"
}
host="{config store name}.azconfig.io"
method="GET"
url="/kv?api-version=1.0"
body=""
credential="<Credential>"
secret="<Secret>"
headers=$(sign_request "$host" "$method" "$url" "$body" "$credential" "$secret")
while IFS= read -r line; do
header_args+=("-H$line")
done <<< "$headers"
curl -X "$method" -d "$body" "${header_args[@]}" "https://$host$url"