# Title `v1.2.0-pre0` - Network Robustness, Fog 1.2.0, Apple Silicon/M1 & Mac Catalyst # Description Added a way for implementing apps to pass in there own `HttpRequester` which can be useful for network robustness. Subspec `LibMobileCoin` now supports Apple Silicon/M1 and Mac Catalyst. Code changes to support Fog v1.2.0. #### `pre1` Changes - Uses the `v1.2.0-pre1` version of `LibMobileCoin` which supports a higher version of `GRPC`. - Added bridging headers - Updated Gemfile/Makefile to use the latest `cocoapods` version `1.11.2` #### Future Work Fix the `docs` steps in the circleci build process. Will require `jazzy` related fixes and testing on Xcode 11. # Changes ## Network Robustness Adds a separate `HTTP` networking architecture. An object conforming to `protocol HttpRequester` can be provided to the `NetworkConfig` object when the `MobileCoinClient` is created. Then the `TransportProtocolOption` can be changed from `grpc` to `http`. > NOTE: This branch will not run because it depends on changes in other submodules that have not yet been merged. ### `HttpRequester` Implementing apps/frameworks should provide an object conforming to this protocol. Our `RestApiRequester` wraps around an `HttpRequester` to communicate with with our services using `protobuf`s. ``` Sources/Network/HttpConnection/HttpRequester.swift ``` ### Attested & Auth Connection Wrappers `HTTP` versions of the Auth & Attested wrapper classes. These protocol and their default implementations handle the logic paths needed for authentication/attestation and re-authentication/attestation: > Can become generic ``` Sources/Network/HttpConnection/ArbitraryHttpConnection.swift Sources/Network/HttpConnection/AttestedHttpConnection.swift ``` ### Networking Protocols Separate code-paths for `HTTP` versions of the networking protocols: ``` Sources/Network/HttpConnection/HttpCallable/AttestedHttpCallable.swift Sources/Network/HttpConnection/HttpCallable/AuthHttpCallable.swift Sources/Network/HttpConnection/HttpCallable/AuthHttpCallableClientWrapper.swift Sources/Network/HttpConnection/HttpCallable/HttpCallable.swift ``` `HTTP` "interfaces" that closely mimic functionality from `GRPC`. Allows us to re-use more of our existing patterns. ``` Sources/Network/HttpConnection/HTTPInterface/HTTPCallOptions.swift Sources/Network/HttpConnection/HTTPInterface/HTTPClient.swift Sources/Network/HttpConnection/HTTPInterface/HTTPClientCall.swift Sources/Network/HttpConnection/HTTPInterface/HTTPMethod.swift Sources/Network/HttpConnection/HTTPInterface/HTTPResponse.swift Sources/Network/HttpConnection/HTTPInterface/HTTPStatus.swift Sources/Network/HttpConnection/HTTPInterface/HTTPUnaryCall.swift ``` ### `HTTP` Connection Implementations Wrapper classes that interface directly with `protoc-swift` generated `.swift` files for our protobuf models. ``` Sources/Network/HttpConnection/HttpConnection.swift Sources/Network/HttpConnection/HttpConnections/BlockchainHttpConnection.swift Sources/Network/HttpConnection/HttpConnections/ConsensusHttpConnection.swift Sources/Network/HttpConnection/HttpConnections/FogBlockHttpConnection.swift Sources/Network/HttpConnection/HttpConnections/FogKeyImageHttpConnection.swift Sources/Network/HttpConnection/HttpConnections/FogMerkleProofHttpConnection.swift Sources/Network/HttpConnection/HttpConnections/FogReportHttpConnection.swift Sources/Network/HttpConnection/HttpConnections/FogUntrustedTxOutHttpConnection.swift Sources/Network/HttpConnection/HttpConnections/FogViewHttpConnection.swift ``` ### `HTTP` versions of `protoc-swift` generated models The GRPC versions of these are generated by `protoc-swift`. The HTTP versions were edited by hand to work with the `HTTP` Connections implementations. > **This could be automated preferably with a `protoc-swift` plugin template but also `sed`/`VIM` if needed.** ``` Sources/Network/HttpConnection/HttpConnections/Http Proto Generated/attest.http.swift Sources/Network/HttpConnection/HttpConnections/Http Proto Generated/consensus_client.http.swift Sources/Network/HttpConnection/HttpConnections/Http Proto Generated/consensus_common.http.swift Sources/Network/HttpConnection/HttpConnections/Http Proto Generated/ledger.http.swift Sources/Network/HttpConnection/HttpConnections/Http Proto Generated/report.http.swift Sources/Network/HttpConnection/HttpConnections/Http Proto Generated/view.http.swift ``` ## Fog Updates The latest version of `fog` changes the name of a protobuf `FogLedger_Block` -> `FogLedger_BlockData`. ### Compressed Commitment The `TxOut` compressed commitment is no longer sent in the protobuf message because it can be reconstructed with its constituent parts (+ the user's `view_private_key`) We now reconstruct the commitment in several places whereas before it was being returned from the decoded protobuf message. Lastly some function signatures into `LibMobileCoin` were updated to adjust to the changes. ## Miscellaneous New MrEnclave values Support for Apple Silicon/M1 & Mac Catalyst ## Unit Tests One unit test was removed. It creates a TxOut and tries to unmask the value with an **incorrect** private view key. The return value should be 'nil' but is noise. It will require a change in the rust code and should be implemented in a future release. Some objects were re-serialized to match the new TxOut structure. Credentials are not required for `consensus` so this has been changed in the `NetworkConfig` Tests can be run with `TransportProtocol == .http` by changing the default value in `NetworkConfig`
370 lines
15 KiB
Swift
370 lines
15 KiB
Swift
//
|
|
// Copyright (c) 2020-2021 MobileCoin. All rights reserved.
|
|
//
|
|
|
|
// swiftlint:disable closure_body_length cyclomatic_complexity function_body_length
|
|
// swiftlint:disable multiline_function_chains operator_usage_whitespace
|
|
|
|
import Foundation
|
|
import LibMobileCoin
|
|
import NIO
|
|
import NIOHPACK
|
|
|
|
enum AttestedHttpConnectionError: Error {
|
|
case connectionError(ConnectionError)
|
|
case attestationFailure(String = String())
|
|
}
|
|
|
|
extension AttestedHttpConnectionError: CustomStringConvertible {
|
|
var description: String {
|
|
"Attested connection error: " + {
|
|
switch self {
|
|
case .connectionError(let connectionError):
|
|
return "\(connectionError)"
|
|
case .attestationFailure(let reason):
|
|
return "Attestation failure\(!reason.isEmpty ? ": \(reason)" : "")"
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
class AttestedHttpConnection: ConnectionProtocol {
|
|
private let requester: RestApiRequester
|
|
private let inner: SerialCallbackLock<Inner>
|
|
|
|
init(
|
|
client: AttestableHttpClient,
|
|
requester: RestApiRequester,
|
|
config: AttestedConnectionConfigProtocol,
|
|
targetQueue: DispatchQueue?,
|
|
rng: (@convention(c) (UnsafeMutableRawPointer?) -> UInt64)? = securityRNG,
|
|
rngContext: Any? = nil
|
|
) {
|
|
let inner = Inner(client: client, requester: requester, config: config, rng: rng, rngContext: rngContext)
|
|
self.requester = requester
|
|
self.inner = .init(inner, targetQueue: targetQueue)
|
|
}
|
|
|
|
func setAuthorization(credentials: BasicCredentials) {
|
|
inner.priorityAccessAsync {
|
|
$0.setAuthorization(credentials: credentials)
|
|
}
|
|
}
|
|
|
|
func performAttestedCall<Call: AttestedHttpCallable>(
|
|
_ call: Call,
|
|
request: Call.InnerRequest,
|
|
completion: @escaping (Result<Call.InnerResponse, ConnectionError>) -> Void
|
|
) where Call.InnerRequestAad == (), Call.InnerResponseAad == () {
|
|
performAttestedCall(call, requestAad: (), request: request, completion: completion)
|
|
}
|
|
|
|
func performAttestedCall<Call: AttestedHttpCallable>(
|
|
_ call: Call,
|
|
requestAad: Call.InnerRequestAad,
|
|
request: Call.InnerRequest,
|
|
completion: @escaping (Result<Call.InnerResponse, ConnectionError>) -> Void
|
|
) where Call.InnerResponseAad == () {
|
|
performAttestedCall(call, requestAad: requestAad, request: request) {
|
|
completion($0.map { $0.response })
|
|
}
|
|
}
|
|
|
|
func performAttestedCall<Call: AttestedHttpCallable>(
|
|
_ call: Call,
|
|
request: Call.InnerRequest,
|
|
completion: @escaping (
|
|
Result<(responseAad: Call.InnerResponseAad, response: Call.InnerResponse),
|
|
ConnectionError>
|
|
) -> Void
|
|
) where Call.InnerRequestAad == () {
|
|
performAttestedCall(call, requestAad: (), request: request, completion: completion)
|
|
}
|
|
|
|
func performAttestedCall<Call: AttestedHttpCallable>(
|
|
_ call: Call,
|
|
requestAad: Call.InnerRequestAad,
|
|
request: Call.InnerRequest,
|
|
completion: @escaping (
|
|
Result<(responseAad: Call.InnerResponseAad, response: Call.InnerResponse),
|
|
ConnectionError>
|
|
) -> Void
|
|
) {
|
|
inner.accessAsync(block: { inner, callback in
|
|
inner.performAttestedCallWithAuth(
|
|
call,
|
|
requestAad: requestAad,
|
|
request: request,
|
|
completion: callback)
|
|
}, completion: completion)
|
|
}
|
|
}
|
|
|
|
extension AttestedHttpConnection {
|
|
// Note: Because `SerialCallbackLock` is being used to wrap `AttestedConnection.Inner`, calls
|
|
// to `AttestedConnection.Inner` have exclusive access (other calls will be queued up) until the
|
|
// executing call invokes the completion handler that returns control back to
|
|
// `AttestedConnection`, at which point the block passed to the async `SerialCallbackLock`
|
|
// method that invoked the call to inner will complete and the next async `SerialCallbackLock`
|
|
// access block will execute.
|
|
//
|
|
// This means that calls to `AttestedConnection.Inner` can assume thread-safety until the call
|
|
// invokes the completion handler.
|
|
private struct Inner {
|
|
private let url: MobileCoinUrlProtocol
|
|
private let session: ConnectionSession
|
|
private let client: AttestableHttpClient
|
|
private let requester: RestApiRequester
|
|
private let attestAke: AttestAke
|
|
|
|
private let responderId: String
|
|
private let attestationVerifier: AttestationVerifier
|
|
private let rng: (@convention(c) (UnsafeMutableRawPointer?) -> UInt64)?
|
|
private let rngContext: Any?
|
|
|
|
init(
|
|
client: AttestableHttpClient,
|
|
requester: RestApiRequester,
|
|
config: AttestedConnectionConfigProtocol,
|
|
rng: (@convention(c) (UnsafeMutableRawPointer?) -> UInt64)? = securityRNG,
|
|
rngContext: Any? = nil
|
|
) {
|
|
self.url = config.url
|
|
self.session = ConnectionSession(config: config)
|
|
self.client = client
|
|
self.requester = requester
|
|
self.attestAke = AttestAke()
|
|
self.responderId = config.url.responderId
|
|
self.attestationVerifier = AttestationVerifier(attestation: config.attestation)
|
|
self.rng = rng
|
|
self.rngContext = rngContext
|
|
}
|
|
|
|
func setAuthorization(credentials: BasicCredentials) {
|
|
session.authorizationCredentials = credentials
|
|
}
|
|
|
|
func performAttestedCallWithAuth<Call: AttestedHttpCallable>(
|
|
_ call: Call,
|
|
requestAad: Call.InnerRequestAad,
|
|
request: Call.InnerRequest,
|
|
completion: @escaping (
|
|
Result<(responseAad: Call.InnerResponseAad, response: Call.InnerResponse),
|
|
ConnectionError>
|
|
) -> Void
|
|
) {
|
|
doPerformAttestedCallWithAuth(
|
|
call,
|
|
requestAad: requestAad,
|
|
request: request,
|
|
attestAkeCipher: attestAke.cipher.map { ($0, freshCipher: false) },
|
|
completion: completion)
|
|
}
|
|
|
|
private func doPerformAttestedCallWithAuth<Call: AttestedHttpCallable>(
|
|
_ call: Call,
|
|
requestAad: Call.InnerRequestAad,
|
|
request: Call.InnerRequest,
|
|
attestAkeCipher: (AttestAke.Cipher, freshCipher: Bool)?,
|
|
completion: @escaping (
|
|
Result<(responseAad: Call.InnerResponseAad, response: Call.InnerResponse),
|
|
ConnectionError>
|
|
) -> Void
|
|
) {
|
|
if let (attestAkeCipher, freshCipher) = attestAkeCipher {
|
|
logger.info(
|
|
"Performing attested call... url: \(self.url)",
|
|
logFunction: false)
|
|
|
|
doPerformAttestedCall(
|
|
call,
|
|
requestAad: requestAad,
|
|
request: request,
|
|
attestAkeCipher: attestAkeCipher
|
|
) {
|
|
switch $0 {
|
|
case .success(let response):
|
|
logger.info(
|
|
"Attested call successful. url: \(self.url)",
|
|
logFunction: false)
|
|
|
|
completion(.success(response))
|
|
case .failure(.connectionError(let connectionError)):
|
|
let errorMessage = "Connection failure while performing attested call. " +
|
|
"url: \(self.url), error: \(connectionError)"
|
|
switch connectionError {
|
|
case .connectionFailure, .serverRateLimited:
|
|
logger.warning(errorMessage, logFunction: false)
|
|
case .authorizationFailure, .invalidServerResponse,
|
|
.attestationVerificationFailed, .outdatedClient:
|
|
logger.error(errorMessage, logFunction: false)
|
|
}
|
|
|
|
completion(.failure(connectionError))
|
|
case .failure(.attestationFailure):
|
|
self.attestAke.deattest()
|
|
|
|
if freshCipher {
|
|
let errorMessage =
|
|
"Attestation failure with fresh auth. url: \(self.url)"
|
|
logger.warning(errorMessage, logFunction: false)
|
|
|
|
completion(.failure(.invalidServerResponse(errorMessage)))
|
|
} else {
|
|
logger.info(
|
|
"Attestation failure using cached auth, reattesting... url: " +
|
|
"\(self.url)",
|
|
logFunction: false)
|
|
|
|
self.doPerformAttestedCallWithAuth(
|
|
call,
|
|
requestAad: requestAad,
|
|
request: request,
|
|
attestAkeCipher: nil,
|
|
completion: completion)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
logger.info(
|
|
"Peforming attestation... url: \(url)",
|
|
logFunction: false)
|
|
|
|
doPerformAuthCall {
|
|
switch $0 {
|
|
case .success(let attestAkeCipher):
|
|
logger.info(
|
|
"Attestation successful. url: \(self.url)",
|
|
logFunction: false)
|
|
|
|
self.doPerformAttestedCallWithAuth(
|
|
call,
|
|
requestAad: requestAad,
|
|
request: request,
|
|
attestAkeCipher: (attestAkeCipher, freshCipher: true),
|
|
completion: completion)
|
|
case .failure(let connectionError):
|
|
let errorMessage = "Connection failure while performing attestation. " +
|
|
"url: \(self.url), error: \(connectionError)"
|
|
switch connectionError {
|
|
case .connectionFailure, .serverRateLimited:
|
|
logger.warning(errorMessage, logFunction: false)
|
|
case .authorizationFailure, .invalidServerResponse,
|
|
.attestationVerificationFailed, .outdatedClient:
|
|
logger.error(errorMessage, logFunction: false)
|
|
}
|
|
|
|
completion(.failure(connectionError))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func doPerformAuthCall(
|
|
completion: @escaping (Result<AttestAke.Cipher, ConnectionError>) -> Void
|
|
) {
|
|
let request = attestAke.authBeginRequest(
|
|
responderId: responderId,
|
|
rng: rng,
|
|
rngContext: rngContext)
|
|
|
|
doPerformCall(
|
|
AuthHttpCallableWrapper(authCallable: client.authCallable, requester: requester),
|
|
request: request
|
|
) {
|
|
completion(
|
|
$0.mapError {
|
|
switch $0 {
|
|
case .connectionError(let connectionError):
|
|
return connectionError
|
|
case .attestationFailure:
|
|
self.attestAke.deattest()
|
|
|
|
return .invalidServerResponse(
|
|
"Attestation failure during auth. url: \(self.url)")
|
|
}
|
|
}.flatMap { response in
|
|
self.attestAke.authEnd(
|
|
authResponse: response,
|
|
attestationVerifier: self.attestationVerifier
|
|
).mapError {
|
|
switch $0 {
|
|
case .invalidInput(let reason):
|
|
return .invalidServerResponse(reason)
|
|
case .attestationVerificationFailed(let reason):
|
|
return .attestationVerificationFailed(reason)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
private func doPerformAttestedCall<Call: AttestedHttpCallable>(
|
|
_ call: Call,
|
|
requestAad: Call.InnerRequestAad,
|
|
request: Call.InnerRequest,
|
|
attestAkeCipher: AttestAke.Cipher,
|
|
completion: @escaping (
|
|
Result<(responseAad: Call.InnerResponseAad, response: Call.InnerResponse),
|
|
AttestedHttpConnectionError>
|
|
) -> Void
|
|
) {
|
|
guard let processedRequest =
|
|
call.processRequest(
|
|
requestAad: requestAad,
|
|
request: request,
|
|
attestAkeCipher: attestAkeCipher)
|
|
.mapError({ _ in .attestationFailure() })
|
|
.successOr(completion: completion)
|
|
else { return }
|
|
|
|
doPerformCall(call, request: processedRequest) {
|
|
completion($0.flatMap { response in
|
|
call.processResponse(response: response, attestAkeCipher: attestAkeCipher)
|
|
})
|
|
}
|
|
}
|
|
|
|
private func doPerformCall<Call: HttpCallable>(
|
|
_ call: Call,
|
|
request: Call.Request,
|
|
completion: @escaping (Result<Call.Response, AttestedHttpConnectionError>) -> Void
|
|
) {
|
|
let callOptions = requestCallOptions()
|
|
|
|
call.call(request: request, callOptions: callOptions) {
|
|
completion(self.processResponse(callResult: $0))
|
|
}
|
|
}
|
|
|
|
private func requestCallOptions() -> HTTPCallOptions {
|
|
HTTPCallOptions(headers: session.requestHeaders)
|
|
}
|
|
|
|
private func processResponse<Response>(callResult: HttpCallResult<Response>)
|
|
-> Result<Response, AttestedHttpConnectionError>
|
|
{
|
|
// Basic credential authorization failure
|
|
guard callResult.status.isOk else {
|
|
return .failure(.connectionError(.authorizationFailure("url: \(url)")))
|
|
}
|
|
|
|
// Attestation failure, reattest
|
|
guard callResult.status.code != 403 else {
|
|
return .failure(.attestationFailure())
|
|
}
|
|
|
|
guard callResult.status.code == 200, let response = callResult.response else {
|
|
return .failure(.connectionError(
|
|
.connectionFailure("url: \(url), status: \(callResult.status.code)")))
|
|
}
|
|
|
|
if let metadata = callResult.metadata {
|
|
session.processResponse(headers: metadata.allHeaderFields)
|
|
}
|
|
|
|
return .success(response)
|
|
}
|
|
}
|
|
}
|