Udostępnij za pośrednictwem


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 idrównież ) i wpisu tajnego (nazywanego valueró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>&SignatureSignedHeaders=<<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"