Skip to content

Commit

Permalink
Add support for Brave Services Key V2 (uplift to 1.62.x) (#21688)
Browse files Browse the repository at this point in the history
Add support for Brave Services Key V2 (#21542)

* Add support for Brave Services Key V2

And use for AI chat

* Refactor

* Move logic to brave_service_keys

* Generalize logic such that callers can sign over multiple headers

* Separate digest header generation

* Rename AI_CHAT_SERVICE_KEY -> SERVICE_KEY_AI_CHAT

* Separate SERVICE_KEY_AI_CHAT from signing logic

* * Switch from base::span<> to const std::vector<>&

* Move unused header to .cc file

* Break apart functions and add more unit tests

* Update GetAuthorizationHeaders

* Pass the URL, HTTP method, full list of headers, and a list of headers
  to actually be signed to GetAuthorizationHeaders

* Instead of using std::vector<std::pair<std::string, std::string>> for
  the list of headers, instead use base::flat_map<std::string,
  std::string> since that matches the headers passed to the
  APIRequestHelper

* Enforce header ordering specified by headers_to_sign

* Generate (request-target) header if supplied, and add test from spec

* Pass url and  method to CreateSignatureString

This way, (request-target) can be generated inside there and thus be
unit tested.

Adjust unit tests.

* Add VLOG(1) when header to sign does not exist

Also DCHECK(false) for good measure.

* Add SERVICE_KEY_AI_CHAT and KEY_ID to config.js

This way they can be sourced from .env.

* Update tests

* Link to specific section test vectors are from

* Remove the "(created)" header from headers_to_sign (it's not included
  in the test vector)

* Use //crypto instad of //crypto:crypto in components/brave_service_keys/BUILD.gn

* Use constexpr for http method constant

* Don't use a reference to the digest header

* Use NOTREACHED_NORETURN() instead of DCHECK and VLOG(1)

* Use CHECK for url in GetAuthorizationHeader

Brave Server URLs should always be defined

* Uncomment base/flat_map.h include in unittest

* Add comment explaining KEY_ID

* Add comments explaining functions in service_key_utils

* Add is_official_build check for service_key_ai_chat

* Update header constants

* Use existing constants for kDigest and kAuthorization

* Change kRequestTarget to kRequestTargetHeader

* Rename service_key_utils.* -> brave_service_key_utils.*

* nit: use base::StrCat and .append()

* Make headers a const& in  CreateSignatureString

* Rename KEY_ID -> BRAVE_SERVICES_KEY_ID

* Fix formatting of string

* Rename SERVICE_KEY_AI_CHAT-> SERVICE_KEY_AICHAT

* Apply Jenkinsfile patch

* Revert "Apply Jenkinsfile patch"

This reverts commit 513bfda.
  • Loading branch information
nvonpentz authored Jan 23, 2024
1 parent ef24eab commit 436d8c7
Show file tree
Hide file tree
Showing 9 changed files with 369 additions and 12 deletions.
4 changes: 4 additions & 0 deletions build/commands/lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ const Config = function () {
this.gomaServerHost.endsWith('.brave.com') ||
this.rbeService.includes('.brave.com:') ||
this.rbeService.includes('.engflow.com:')
this.brave_services_key_id = getNPMConfig(['brave_services_key_id']) || ''
this.service_key_aichat = getNPMConfig(['service_key_aichat']) || ''
}

Config.prototype.isReleaseBuild = function () {
Expand Down Expand Up @@ -408,6 +410,8 @@ Config.prototype.buildArgs = function () {
brave_services_staging_domain: this.braveServicesStagingDomain,
brave_services_dev_domain: this.braveServicesDevDomain,
enable_dangling_raw_ptr_checks: this.enable_dangling_raw_ptr_checks,
brave_services_key_id: this.brave_services_key_id,
service_key_aichat: this.service_key_aichat,
...this.extraGnArgs,
}

Expand Down
5 changes: 3 additions & 2 deletions components/ai_chat/core/browser/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ static_library("browser") {
sources = [
"ai_chat_credential_manager.cc",
"ai_chat_credential_manager.h",
"conversation_driver.cc",
"conversation_driver.h",
"ai_chat_feedback_api.cc",
"ai_chat_feedback_api.h",
"ai_chat_metrics.cc",
"ai_chat_metrics.h",
"constants.cc",
"constants.h",
"conversation_driver.cc",
"conversation_driver.h",
"engine/engine_consumer.h",
"engine/engine_consumer_claude.cc",
"engine/engine_consumer_claude.h",
Expand All @@ -37,6 +37,7 @@ static_library("browser") {
"//brave/components/ai_chat/core/common/buildflags",
"//brave/components/ai_chat/core/common/mojom",
"//brave/components/api_request_helper",
"//brave/components/brave_service_keys",
"//brave/components/brave_stats/browser",
"//brave/components/constants",
"//brave/components/l10n/common",
Expand Down
30 changes: 21 additions & 9 deletions components/ai_chat/core/browser/engine/remote_completion_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "brave/components/ai_chat/core/browser/constants.h"
#include "brave/components/ai_chat/core/common/buildflags/buildflags.h"
#include "brave/components/ai_chat/core/common/features.h"
#include "brave/components/brave_service_keys/brave_service_key_utils.h"
#include "brave/components/constants/brave_services_key.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
Expand All @@ -31,7 +32,8 @@
namespace ai_chat {
namespace {

constexpr char kAIChatCompletionPath[] = "v1/complete";
constexpr char kAIChatCompletionPath[] = "v2/complete";
constexpr char kHttpMethod[] = "POST";

net::NetworkTrafficAnnotationTag GetNetworkTrafficAnnotationTag() {
return net::DefineNetworkTrafficAnnotation("ai_chat", R"(
Expand Down Expand Up @@ -146,7 +148,23 @@ void RemoteCompletionClient::OnFetchPremiumCredential(
absl::optional<CredentialCacheEntry> credential) {
bool premium_enabled = credential.has_value();
const GURL api_url = GetEndpointUrl(premium_enabled, kAIChatCompletionPath);
const bool is_sse_enabled =
ai_chat::features::kAIChatSSE.Get() && !data_received_callback.is_null();
const base::Value::Dict& dict =
CreateApiParametersDict(prompt, model_name_, stop_sequences_,
std::move(extra_stop_sequences), is_sse_enabled);
const std::string request_body = CreateJSONRequestBody(dict);

base::flat_map<std::string, std::string> headers;
const auto digest_header = brave_service_keys::GetDigestHeader(request_body);
headers.emplace(digest_header.first, digest_header.second);
auto result = brave_service_keys::GetAuthorizationHeader(
BUILDFLAG(SERVICE_KEY_AICHAT), headers, api_url, kHttpMethod, {"digest"});
if (result) {
std::pair<std::string, std::string> authorization_header = result.value();
headers.emplace(authorization_header.first, authorization_header.second);
}

if (premium_enabled) {
// Add Leo premium SKU credential as a Cookie header.
std::string cookie_header_value =
Expand All @@ -156,12 +174,6 @@ void RemoteCompletionClient::OnFetchPremiumCredential(
headers.emplace("x-brave-key", BUILDFLAG(BRAVE_SERVICES_KEY));
headers.emplace("Accept", "text/event-stream");

const bool is_sse_enabled =
ai_chat::features::kAIChatSSE.Get() && !data_received_callback.is_null();

const base::Value::Dict& dict =
CreateApiParametersDict(prompt, model_name_, stop_sequences_,
std::move(extra_stop_sequences), is_sse_enabled);
if (is_sse_enabled) {
VLOG(2) << "Making streaming AI Chat API Request";
auto on_received = base::BindRepeating(
Expand All @@ -172,7 +184,7 @@ void RemoteCompletionClient::OnFetchPremiumCredential(
weak_ptr_factory_.GetWeakPtr(), credential,
std::move(data_completed_callback));

api_request_helper_.RequestSSE("POST", api_url, CreateJSONRequestBody(dict),
api_request_helper_.RequestSSE(kHttpMethod, api_url, request_body,
"application/json", std::move(on_received),
std::move(on_complete), headers, {});
} else {
Expand All @@ -182,7 +194,7 @@ void RemoteCompletionClient::OnFetchPremiumCredential(
weak_ptr_factory_.GetWeakPtr(), credential,
std::move(data_completed_callback));

api_request_helper_.Request("POST", api_url, CreateJSONRequestBody(dict),
api_request_helper_.Request(kHttpMethod, api_url, request_body,
"application/json", std::move(on_complete),
headers, {});
}
Expand Down
13 changes: 12 additions & 1 deletion components/ai_chat/core/common/buildflags/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,20 @@ import("//brave/build/config.gni")
import("//brave/components/ai_chat/core/common/buildflags/buildflags.gni")
import("//build/buildflag_header.gni")

declare_args() {
service_key_aichat = ""
}

if (is_official_build) {
assert(service_key_aichat != "")
}

buildflag_header("buildflags") {
header = "buildflags.h"
flags = [ "ENABLE_AI_CHAT=$enable_ai_chat" ]
flags = [
"ENABLE_AI_CHAT=$enable_ai_chat",
"SERVICE_KEY_AICHAT=\"$service_key_aichat\"",
]

# Enable for desktop (all channels) and android (only dev and
# nightly channels).
Expand Down
51 changes: 51 additions & 0 deletions components/brave_service_keys/BUILD.gn
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright (c) 2024 The Brave Authors. All rights reserved.
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at https://mozilla.org/MPL/2.0/.

import("//brave/build/config.gni")
import("//build/buildflag_header.gni")
import("//testing/test.gni")

declare_args() {
# BRAVE_SERVICES_KEY_ID = TARGET_OS + '-' + CV_MAJOR + '-' + RELEASE_CHANNEL
# It is used (in combination with a secret seed) to generate service keys
# for each service, OS, chrominum version, release channel
# combination.
brave_services_key_id = ""
}

if (is_official_build) {
assert(brave_services_key_id != "")
}

buildflag_header("buildflags") {
header = "buildflags.h"
flags = [ "BRAVE_SERVICES_KEY_ID=\"$brave_services_key_id\"" ]
}

static_library("brave_service_keys") {
sources = [
"brave_service_key_utils.cc",
"brave_service_key_utils.h",
]

deps = [
":buildflags",
"//base",
"//crypto",
"//net",
"//url:url",
]
}
source_set("unit_tests") {
testonly = true
sources = [ "brave_service_key_utils_unittest.cc" ]

deps = [
":brave_service_keys",
":buildflags",
"//base",
"//testing/gtest",
]
}
110 changes: 110 additions & 0 deletions components/brave_service_keys/brave_service_key_utils.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright (c) 2024 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

#include "brave/components/brave_service_keys/brave_service_key_utils.h"

#include <vector>

#include "base/base64.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "brave/components/brave_service_keys/buildflags.h"
#include "crypto/hmac.h"
#include "crypto/sha2.h"
#include "net/http/http_auth_scheme.h"
#include "net/http/http_request_headers.h"

namespace brave_service_keys {

namespace {

constexpr char kRequestTargetHeader[] = "(request-target)";

} // namespace

std::pair<std::string, std::string> GetDigestHeader(
const std::string& payload) {
const std::string value = base::StrCat(
{"SHA-256=", base::Base64Encode(crypto::SHA256HashString(payload))});
return std::make_pair(net::kDigestAuthScheme, value);
}

std::pair<std::string, std::string> CreateSignatureString(
const base::flat_map<std::string, std::string>& headers,
const GURL& url,
const std::string& method,
const std::vector<std::string>& headers_to_sign) {
std::string header_names;
std::string signature_string;

for (const auto& header_to_sign : headers_to_sign) {
// Prepend some padding / newlines if this isn't the first
// header to sign
if (!header_names.empty()) {
header_names.append(" ");
signature_string.append("\n");
}
header_names.append(header_to_sign);

// Handle the special case header (request-target) by constructing
// the value instead of getting it from headers.
if (header_to_sign == kRequestTargetHeader) {
signature_string.append(
base::StrCat({kRequestTargetHeader, ": ", base::ToLowerASCII(method),
" ", url.PathForRequest()}));
continue;
}

// For all the headers to sign, we expect their values to be be in the
// headers flat_map and use the value there to add to the signature string.
auto header = headers.find(header_to_sign);
if (header == headers.end()) {
NOTREACHED_NORETURN()
<< "Can't sign over non-existent header " << header_to_sign;
}
signature_string.append(
base::StrCat({header_to_sign, ": ", header->second}));
}

return std::make_pair(header_names, signature_string);
}

std::optional<std::pair<std::string, std::string>> GetAuthorizationHeader(
const std::string& service_key,
const base::flat_map<std::string, std::string>& headers,
const GURL& url,
const std::string& method,
const std::vector<std::string>& headers_to_sign) {
CHECK(url.is_valid());
auto [header_names, signature_string] =
CreateSignatureString(headers, url, method, headers_to_sign);

// Create the signature using the service_key.
crypto::HMAC hmac(crypto::HMAC::SHA256);
const size_t signature_digest_length = hmac.DigestLength();
std::vector<uint8_t> signature_digest(signature_digest_length);
const bool success = hmac.Init(service_key) &&
hmac.Sign(signature_string, &signature_digest[0],
signature_digest.size());
if (!success) {
return std::nullopt;
}

// Create the authorization header.
std::string signature_digest_base64;
base::Base64Encode(
std::string(signature_digest.begin(), signature_digest.end()),
&signature_digest_base64);

const std::string value =
base::StrCat({"Signature keyId=\"", BUILDFLAG(BRAVE_SERVICES_KEY_ID),
"\",algorithm=\"hs2019\",headers=\"", header_names,
"\",signature=\"", signature_digest_base64, "\""});

return std::make_pair(net::HttpRequestHeaders::kAuthorization, value);
}

} // namespace brave_service_keys
44 changes: 44 additions & 0 deletions components/brave_service_keys/brave_service_key_utils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) 2024 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

#ifndef BRAVE_COMPONENTS_BRAVE_SERVICE_KEYS_BRAVE_SERVICE_KEY_UTILS_H_
#define BRAVE_COMPONENTS_BRAVE_SERVICE_KEYS_BRAVE_SERVICE_KEY_UTILS_H_

#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "base/containers/flat_map.h"
#include "url/gurl.h"

namespace brave_service_keys {

// Calculates the SHA-256 hash of the supplied payload and returns a pair
// comprising of the digest header field, and header value in the format
// "SHA-256=<base64_encoded_hash>".
std::pair<std::string, std::string> GetDigestHeader(const std::string& payload);

// Generates the the string to be signed over and included in the authorization
// header. See
// https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-08#section-2.3:w
std::pair<std::string, std::string> CreateSignatureString(
const base::flat_map<std::string, std::string>& headers,
const GURL& url,
const std::string& method,
const std::vector<std::string>& headers_to_sign);

// Generates an authorization header field and value pair using the provided
// service key to sign over specified headers.
std::optional<std::pair<std::string, std::string>> GetAuthorizationHeader(
const std::string& service_key,
const base::flat_map<std::string, std::string>& headers,
const GURL& url,
const std::string& method,
const std::vector<std::string>& headers_to_sign);

} // namespace brave_service_keys

#endif // BRAVE_COMPONENTS_BRAVE_SERVICE_KEYS_BRAVE_SERVICE_KEY_UTILS_H_
Loading

0 comments on commit 436d8c7

Please sign in to comment.