Skip to content

Commit

Permalink
[skip ci] WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
NachoSoto committed Dec 8, 2023
1 parent 27138c4 commit 2fea96d
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 41 deletions.
4 changes: 4 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@
4FE6FEEB2AA940E300780B45 /* PaywallEventSerializerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6FEE82AA940E300780B45 /* PaywallEventSerializerTests.swift */; };
4FF017C32AB378A7004976EB /* BaseStoreKitIntegrationTests+Verification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF017C22AB378A7004976EB /* BaseStoreKitIntegrationTests+Verification.swift */; };
4FF017C42AB378A7004976EB /* BaseStoreKitIntegrationTests+Verification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF017C22AB378A7004976EB /* BaseStoreKitIntegrationTests+Verification.swift */; };
4FF6E4B92B069B8C000141ED /* HTTPRequest+Signing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF6E4B82B069B8C000141ED /* HTTPRequest+Signing.swift */; };
4FF8464D2A32554300617F00 /* DiagnosticsStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF8464C2A32554300617F00 /* DiagnosticsStrings.swift */; };
4FFCED822AA941B200118EF4 /* PaywallEventsRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FFCED802AA941B200118EF4 /* PaywallEventsRequestTests.swift */; };
4FFCED832AA941B200118EF4 /* PaywallEventsBackendTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FFCED812AA941B200118EF4 /* PaywallEventsBackendTests.swift */; };
Expand Down Expand Up @@ -1075,6 +1076,7 @@
4FE6FEE82AA940E300780B45 /* PaywallEventSerializerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaywallEventSerializerTests.swift; sourceTree = "<group>"; };
4FED3AD62AAA7DD4001D4D5E /* purchases-ios */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "purchases-ios"; path = ..; sourceTree = "<group>"; };
4FF017C22AB378A7004976EB /* BaseStoreKitIntegrationTests+Verification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseStoreKitIntegrationTests+Verification.swift"; sourceTree = "<group>"; };
4FF6E4B82B069B8C000141ED /* HTTPRequest+Signing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HTTPRequest+Signing.swift"; sourceTree = "<group>"; };
4FF8464C2A32554300617F00 /* DiagnosticsStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsStrings.swift; sourceTree = "<group>"; };
4FFCED802AA941B200118EF4 /* PaywallEventsRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaywallEventsRequestTests.swift; sourceTree = "<group>"; };
4FFCED812AA941B200118EF4 /* PaywallEventsBackendTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaywallEventsBackendTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2604,6 +2606,7 @@
5740FCD22996CE5E00E049F9 /* VerificationResult.swift */,
4F6EEBD82A38ED76007FD783 /* FakeSigning.swift */,
4F8452672A5756CC00084550 /* HTTPRequestBody+Signing.swift */,
4FF6E4B82B069B8C000141ED /* HTTPRequest+Signing.swift */,
);
path = Security;
sourceTree = "<group>";
Expand Down Expand Up @@ -3405,6 +3408,7 @@
5751379527F4C4D80064AB2C /* Optional+Extensions.swift in Sources */,
B3852FA026C1ED1F005384F8 /* IdentityManager.swift in Sources */,
9A65E03625918B0500DE00B0 /* ConfigureStrings.swift in Sources */,
4FF6E4B92B069B8C000141ED /* HTTPRequest+Signing.swift in Sources */,
57488C7729CB90F90000EE7E /* PurchasedProductsFetcher.swift in Sources */,
4F3C986A2A44FA60009AECA3 /* ErrorResponse.swift in Sources */,
578C5F2C28DB82DD00A56F02 /* PurchasesDiagnostics.swift in Sources */,
Expand Down
17 changes: 15 additions & 2 deletions Sources/Networking/HTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,22 @@ extension HTTPClient {
}
}

static func headerParametersForSignatureHeader(with headers: RequestHeaders) -> RequestHeaders {
if let header = HTTPRequest.headerParametersForSignatureHeader(headers: headers) {
return [RequestHeader.headerParametersForSignature.rawValue: header]
} else {
return [:]
}
}

enum RequestHeader: String {

case authorization = "Authorization"
case nonce = "X-Nonce"
case eTag = "X-RevenueCat-ETag"
case eTagValidationTime = "X-RC-Last-Refresh-Time"
case postParameters = "X-Post-Params-Hash"
case headerParametersForSignature = "X-Header-Params-Hash"
case sandbox = "X-Is-Sandbox"

}
Expand Down Expand Up @@ -313,6 +322,7 @@ private extension HTTPClient {
return cachedResponse.verify(
signing: self.signing(for: request.httpRequest),
request: request.httpRequest,
requestHeaders: request.headers,
publicKey: request.verificationMode.publicKey
)
}
Expand Down Expand Up @@ -532,10 +542,13 @@ extension HTTPRequest {

if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *),
verificationMode.isEnabled,
self.path.supportsSignatureVerification,
let body = self.requestBody {
self.path.supportsSignatureVerification {
result += HTTPClient.headerParametersForSignatureHeader(with: defaultHeaders)

if let body = self.requestBody {
result += HTTPClient.postParametersHeaderForSigning(with: body)
}
}

return result
}
Expand Down
91 changes: 91 additions & 0 deletions Sources/Security/HTTPRequest+Signing.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// HTTPRequest+Signing.swift
//
// Created by Nacho Soto on 11/16/23.

import CryptoKit
import Foundation

extension HTTPRequest {

static func headerParametersForSignatureHeader(headers: Headers) -> String? {
guard #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) else {
// Signature verification is not available.
return nil
}

let headersToSign = Self.headersToSign.map(\.rawValue)

if !headersToSign.isEmpty, let hash = Self.postParameterHash(headers) {
return Self.signatureHashHeader(keys: headersToSign, hash: hash)
} else {
return nil
}
}

/// - Returns: `nil` if none of the requested headers are found
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
private static func postParameterHash(_ headers: Headers) -> String? {
let headersToSign = Self.headersToSign.map(\.rawValue)
let values = headers
.filter { headersToSign.contains($0.key) }
.map(\.value)

guard !values.isEmpty else { return nil }

return Self.signingParameterHash(values)
}

}

extension HTTPRequest {

static func signatureHashHeader(
keys: [String],
hash: String
) -> String {
return [
keys.joined(separator: ","),
postParameterHashingAlgorithmName,
hash
].joined(separator: ":")
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
static func signingParameterHash(_ values: [String]) -> String {
var sha256 = SHA256()

for (index, value) in values.enumerated() {
if index > 0 {
sha256.update(data: fieldSeparator)
}

sha256.update(data: value.asData)
}

return sha256.toString()
}

}

// MARK: - Private

private extension HTTPRequest {

/// Ordered list of header keys that will be included in the signature.
static let headersToSign: [HTTPClient.RequestHeader] = [
.sandbox
]

}

private let postParameterHashingAlgorithmName = "sha256"
private let fieldSeparator = Data(bytes: [0x00], count: 1)
28 changes: 3 additions & 25 deletions Sources/Security/HTTPRequestBody+Signing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
//
// Created by Nacho Soto on 7/6/23.

import CryptoKit
import Foundation

extension HTTPRequestBody {
Expand All @@ -27,30 +26,12 @@ extension HTTPRequestBody {
return nil
}

let pieces = [
keys.joined(separator: ","),
postParameterHashingAlgorithmName,
self.postParameterHash
]

return pieces.joined(separator: ":")
return HTTPRequest.signatureHashHeader(keys: keys, hash: self.postParameterHash)
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
var postParameterHash: String {
var sha256 = SHA256()

let values = self.contentForSignature.map(\.value)

for (index, value) in values.enumerated() {
if index > 0 {
sha256.update(data: fieldSeparator)
}

sha256.update(data: value.asData)
}

return sha256.toString()
private var postParameterHash: String {
return HTTPRequest.signingParameterHash(self.contentForSignature.map(\.value))
}

}
Expand All @@ -63,6 +44,3 @@ private extension HTTPRequestBody {
}

}

private let postParameterHashingAlgorithmName = "sha256"
private let fieldSeparator = Data(bytes: [0x00], count: 1)
12 changes: 8 additions & 4 deletions Sources/Security/Signing+ResponseVerification.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ extension HTTPResponse where Body == Data? {
func verify(
signing: SigningType,
request: HTTPRequest,
requestHeaders: HTTPRequest.Headers,
publicKey: Signing.PublicKey?
) -> VerifiedHTTPResponse<Body> {
let verificationResult = Self.verificationResult(
body: self.body,
statusCode: self.httpStatusCode,
headers: self.responseHeaders,
requestHeaders: requestHeaders,
responseHeaders: self.responseHeaders,
requestDate: self.requestDate,
request: request,
publicKey: publicKey,
Expand All @@ -48,7 +50,8 @@ extension HTTPResponse where Body == Data? {
private static func verificationResult(
body: Data?,
statusCode: HTTPStatusCode,
headers: HTTPClient.ResponseHeaders,
requestHeaders: HTTPClient.RequestHeaders,
responseHeaders: HTTPClient.ResponseHeaders,
requestDate: Date?,
request: HTTPRequest,
publicKey: Signing.PublicKey?,
Expand All @@ -62,7 +65,7 @@ extension HTTPResponse where Body == Data? {

guard let signature = HTTPResponse.value(
forCaseInsensitiveHeaderField: .signature,
in: headers
in: responseHeaders
) else {
if request.path.supportsSignatureVerification {
Logger.warn(Strings.signing.signature_was_requested_but_not_provided(request))
Expand All @@ -82,9 +85,10 @@ extension HTTPResponse where Body == Data? {
with: .init(
path: request.path,
message: body,
requestHeaders: requestHeaders,
requestBody: request.requestBody,
nonce: request.nonce,
etag: HTTPResponse.value(forCaseInsensitiveHeaderField: .eTag, in: headers),
etag: HTTPResponse.value(forCaseInsensitiveHeaderField: .eTag, in: responseHeaders),
requestDate: requestDate.millisecondsSince1970
),
publicKey: publicKey) {
Expand Down
12 changes: 12 additions & 0 deletions Sources/Security/Signing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
//
// Created by Nacho Soto on 1/13/23.

// swiftlint:disable file_length

import CryptoKit
import Foundation

Expand All @@ -37,6 +39,7 @@ final class Signing: SigningType {

var path: HTTPRequestPath
var message: Data?
var requestHeaders: HTTPRequest.Headers
var requestBody: HTTPRequestBody?
var nonce: Data?
var etag: String?
Expand Down Expand Up @@ -246,13 +249,15 @@ extension Signing.SignatureParameters {
init(
path: HTTPRequest.Path,
message: Data? = nil,
requestHeaders: HTTPRequest.Headers = [:],
requestBody: HTTPRequestBody? = nil,
nonce: Data? = nil,
etag: String? = nil,
requestDate: UInt64
) {
self.path = path
self.message = message
self.requestHeaders = requestHeaders
self.requestBody = requestBody
self.nonce = nonce
self.etag = etag
Expand All @@ -267,6 +272,8 @@ extension Signing.SignatureParameters {
var asData: Data {
let nonce: Data = self.nonce ?? .init()
let path: Data = self.path.relativePath.asData
let headerParametersHash: Data = HTTPRequest.headerParametersForSignatureHeader(headers: self.requestHeaders)?
.asData ?? .init()
let postParameterHash: Data = self.requestBody?.postParameterHeader?.asData ?? .init()
let requestDate: Data = String(self.requestDate).asData
let etag: Data = (self.etag ?? "").asData
Expand All @@ -275,6 +282,7 @@ extension Signing.SignatureParameters {
return (
nonce +
path +
headerParametersHash +
postParameterHash +
requestDate +
etag +
Expand All @@ -291,6 +299,10 @@ extension Signing.SignatureParameters: CustomDebugStringConvertible {
SignatureParameters(" +
path: '\(self.path.relativePath)'
message: '\(self.messageString.trimmingWhitespacesAndNewLines)'
headerParametersHash: '\(HTTPRequest.headerParametersForSignatureHeader(
headers: self.requestHeaders
) ?? "")'
headers: '\(self.requestHeaders)'
postParameterHeader: '\(self.requestBody?.postParameterHeader ?? "")'
nonce: '\(self.nonce?.base64EncodedString() ?? "")'
etag: '\(self.etag ?? "")'
Expand Down
2 changes: 2 additions & 0 deletions Tests/UnitTests/Mocks/MockHTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ class MockHTTPClient: HTTPClient {
.requestAddingNonceIfRequired(with: verificationMode)
.withHardcodedNonce

isRecording = true

let call = Call(request: request,
headers: request.headers(with: self.authHeaders,
defaultHeaders: self.defaultHeaders,
Expand Down
21 changes: 18 additions & 3 deletions Tests/UnitTests/Networking/HTTPResponseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ class HTTPResponseTests: TestCase {
responseHeaders: [:],
body: Data()
)
let verifiedResponse = response.verify(signing: Self.signing, request: request, publicKey: nil)
let verifiedResponse = response.verify(
signing: Self.signing,
request: request,
requestHeaders: [:],
publicKey: nil
)

expect(verifiedResponse.verificationResult) == .notRequested
}
Expand All @@ -45,7 +50,12 @@ class HTTPResponseTests: TestCase {
responseHeaders: [:],
body: Data()
)
let verifiedResponse = response.verify(signing: Self.signing, request: request, publicKey: key)
let verifiedResponse = response.verify(
signing: Self.signing,
request: request,
requestHeaders: [:],
publicKey: key
)

expect(verifiedResponse.verificationResult) == .notRequested
}
Expand All @@ -62,7 +72,12 @@ class HTTPResponseTests: TestCase {
responseHeaders: [:],
body: Data()
)
let verifiedResponse = response.verify(signing: Self.signing, request: request, publicKey: key)
let verifiedResponse = response.verify(
signing: Self.signing,
request: request,
requestHeaders: [:],
publicKey: key
)

expect(verifiedResponse.verificationResult) == .failed
}
Expand Down
Loading

0 comments on commit 2fea96d

Please sign in to comment.