Bloombox/Swift

View on GitHub
Sources/Client/IdentityClient.swift

Summary

Maintainability
A
1 hr
Test Coverage
/**
* Copyright 2019, Momentum Ideas, Co. All rights reserved.
* Source and object computer code contained herein is the private intellectual
* property of Momentum Ideas Co., a Delaware Corporation. Use of this
* code in source form requires permission in writing before use or the
* assembly, distribution, or publishing of derivative works, for commercial
* purposes or any other purpose, from a duly authorized officer of Momentum
* Ideas Co.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/

import Foundation
import SwiftGRPC


// Type Aliases


public typealias KeyID = String


// Callback Types

/// Callback type definition for an identity resolution routine, where public key material is fetched according to its
/// opaque key ID, or cryptographic fingerprint.
///
/// - Parameters:
///    - `CallResult`: gRPC call result object, which includes a status code.
///    - `ResolveKey.Response?`: If the call succeeded, a key resolution response, else `nil`.
///    - `Error?`: If an error occurred before request transmission, or server-side, it is provided here.
public typealias IdentityResolveCallback = (CallResult?, ResolveKey.Response?, Error?) -> ()


/// Enumerates code-level errors in the identity client.
public enum IdentityClientError: Error {
  /// No API key could be resolved, or the given API key was invalid.
  case invalidApiKey

  /// Thrown when a key is attempted for resolution with a fingerprint that uses an unsupported algorithm.
  case unsupportedAlgorithm

  /// Thrown when digest content cannot be resolved.
  case invalidDigest

  /// An unknown client-side error occurred.
  case unknown
}

/// Provides functionality for the Identity API, which is a simple service that supports resolution of arbitrary public
/// key material, previously registered by the agent that controls each key, by the public key's opaque key ID or
/// cryptographic fingerprint. In some circumstances, key ownership information is provided as well.
///
/// The main function of the Identity API is to resolve public keys for the purpose of verifying signatures. In most
/// cases, a signature in the Bloombox ecosystem includes the fingerprint of the public key portion of the asymmetric
/// keypair that performed the signing.
public final class IdentityClient: RemoteService {
  /// Name of the Identity API, which is "identity".
  let name = "identity"

  /// Version of this service.
  let version = "v1beta1"

  // MARK: Internals

  /// Client-wide settings.
  internal let settings: Bloombox.Settings

  /// Low-level identity service.
  internal var svc: IdentityService?

  /// Library-internal initializer.
  ///
  /// - Parameter settings: Client-wide settings to apply.
  internal init(settings: Bloombox.Settings) {
    self.settings = settings
  }

  /// Identity service. Retrieve an implementation of the identity service, capable of communicating with server-side
  /// methods for resolving public key material.
  ///
  /// - Parameter apiKey: API Key to use.
  /// - Parameter settings: Combined settings to use.
  /// - Returns: Prepared Identity API service class.
  /// - Throws: Client-side errors. See: `IdentityClientError`.
  private func service(_ apiKey: APIKey) throws -> IdentityService {
    if let s = self.svc {
      return s
    }
    let svc = RPCServiceFactory<IdentityService>.factory(
      forService: Transport.config.services.identity,
      withSettings: self.settings)

    try svc.metadata.add(key: "x-api-key", value: apiKey)
    self.svc = svc
    return svc
  }

  /// Resolve method context, throwing an error if it cannot be figured out. Where identity ops are concerned, this
  /// only includes the API key with which we should connect to the service.
  ///
  /// - Parameter apiKey: API key to connect to the service with.
  /// - Returns: API key to use, based either on the override or library-default value.
  /// - Throws: Client-side errors. See: `IdentityClientError`.
  private func resolveContext(_ apiKey: APIKey? = nil) throws -> APIKey {
    let apiKey: APIKey? = apiKey ?? settings.apiKey
    
    guard apiKey != nil else {
      throw IdentityClientError.invalidApiKey
    }
    return apiKey!
  }

  /// Resolve cryptographic hex digest from a `Hash` data structure, for the purpose of resolving a key.
  ///
  /// - Parameter fingerprint: Cryptographic fingerprint structure.
  /// - Returns: Hash algorithm in use and resolved hexdigest.
  /// - Throws: `IdentityClientError.invalidDigest` if digest content cannot be resolved.
  private func resolveHexdigest(_ fingerprint: Hash) throws -> (algorithm: HashAlgorithm, content: String) {
    guard fingerprint.algorithm == .sha1 || fingerprint.algorithm == .md5 else {
      throw IdentityClientError.unsupportedAlgorithm
    }
    
    guard let digest = fingerprint.digest else {
      throw IdentityClientError.invalidDigest
    }
    
    // we have some kind of digest content
    let subject: String
    switch digest {
    case .hex(let content):
      subject = content.uppercased().trimmingCharacters(in: .whitespacesAndNewlines)
    default:
      throw IdentityClientError.invalidDigest
    }
    return (algorithm: fingerprint.algorithm, content: subject)
  }

  // MARK: - Public API -

  // MARK: Key Resolution

  /// Resolve public key material by the key pair's assigned or generated opaque key ID. In some circumstances,
  /// public keys are resolved by an ID (for instance when JWT or JWS structures are involved). Keypairs are
  /// registered by the agent that originally retains custody of the key.
  ///
  /// - Parameter id: Opaque key ID to retrieve.
  /// - Parameter apiKey: Override the API key set in library defaults.
  /// - Returns: Key material response. This method is synchronous, so a response is returned directly.
  /// - Throws: `IdentityClientError` codes related to the API key or other circumstances.
  public func publicKey(byID id: KeyID,
                        authorizedBy apiKey: APIKey? = nil) throws -> ResolveKey.Response {
    return try self.service(try resolveContext(apiKey)).keyResolve(ResolveKey.Request.with { builder in
      builder.keyID = id
    })
  }

  /// Resolve public key material, asynchronously, by the key pair's assigned or generated opaque key ID. In some
  /// circumstances, public keys are resolved by an ID (for i nstance when JWT or JWS structures are involved).
  /// Keypairs are registered by the agent that originally retains custody of the key.
  ///
  /// - Parameter id: Opaque key ID to retrieve.
  /// - Parameter apiKey: Override the API key set in library defaults.
  /// - Parameter callback: Callable to dispatch once either a response or terminal error is available.
  /// - Returns: RPC call object, which can be observed or cancelled.
  /// - Throws: `IdentityClientError` codes related to the API key or other circumstances.
  @discardableResult
  public func publicKey(byID id: KeyID,
                        authorizedBy apiKey: APIKey? = nil,
                        _ callback: @escaping IdentityResolveCallback) throws -> ResolveKeyCall {
    return try self.service(try resolveContext(apiKey)).keyResolve(ResolveKey.Request.with { builder in
      builder.keyID = id
    }) { (response, callResult) in
      callback(callResult, response, nil)
    }
  }

  /// Resolve public key material, synchronously, by the subject keypair's public key cryptographic
  /// fingerprint. In circumstances where a signature is present (for instance, a transaction), the asymmetric
  /// keypair that performed the signature is referenced by a cryptographic fingerprint of the key pair's
  /// constituent public key. This method allows fetching of the backing public key, so it may be used to
  /// verify signatures associated with the key pair.
  ///
  /// - Parameter fingerprint: Key fingerprint to retrieve. Only SHA1 and MD5 are supported, for now.
  /// - Parameter apiKey: Override the API key set in library defaults.
  /// - Returns: Key material response. This method is synchronous, so a response is returned directly.
  /// - Throws: `IdentityClientError` codes related to the API key or other circumstances.
  /// - Throws: `IdentityClientError.invalidDigest` if the digest content cannot be resolved.
  /// - Throws: `IdentityCLientError.unsupportedAlgorithm` if the fingerprint structure uses an unsupported
  ///           algorithm. At the time of writing, SHA1 and MD5 are supported.
  public func publicKey(fingerprint: Hash,
                        authorizedBy apiKey: APIKey? = nil) throws -> ResolveKey.Response {
    let (algorithm, subject) = try self.resolveHexdigest(fingerprint)
    return try self.service(try resolveContext(apiKey)).keyResolve(ResolveKey.Request.with { builder in
      // we support SHA1 and MD5 here, for now
      switch algorithm {
      case .sha1: builder.sha1 = subject
      case .md5: builder.md5 = subject
      default: throw IdentityClientError.unsupportedAlgorithm
      }
    })
  }

  /// Resolve public key material, asynchronously, by the subject keypair's public key cryptographic
  /// fingerprint. In circumstances where a signature is present (for instance, a transaction), the asymmetric
  /// keypair that performed the signature is referenced by a cryptographic fingerprint of the key pair's
  /// constituent public key. This method allows fetching of the backing public key, so it may be used to
  /// verify signatures associated with the key pair.
  ///
  /// - Parameter fingerprint: Key fingerprint to retrieve. Only SHA1 and MD5 are supported, for now.
  /// - Parameter apiKey: Override the API key set in library defaults.
  /// - Parameter callback: Callable to dispatch once either a response or terminal error is available.
  /// - Returns: RPC call object, which can be observed or cancelled.
  /// - Throws: `IdentityClientError` codes related to the API key or other circumstances.
  /// - Throws: `IdentityClientError.invalidDigest` if the digest content cannot be resolved.
  /// - Throws: `IdentityCLientError.unsupportedAlgorithm` if the fingerprint structure uses an unsupported
  ///           algorithm. At the time of writing, SHA1 and MD5 are supported.
  public func publicKey(fingerprint: Hash,
                        authorizedBy apiKey: APIKey? = nil,
                        _ callback: @escaping IdentityResolveCallback) throws -> ResolveKeyCall {
    let (algorithm, subject) = try self.resolveHexdigest(fingerprint)
    return try self.service(try resolveContext(apiKey)).keyResolve(ResolveKey.Request.with { builder in
      // we support SHA1 and MD5 here, for now
      switch algorithm {
      case .sha1: builder.sha1 = subject
      case .md5: builder.md5 = subject
      default: throw IdentityClientError.unsupportedAlgorithm
      }
    }) { (response, callResult) in
      callback(callResult, response, nil)
    }
  }
}