Skip to content

Commit

Permalink
[Swift] functionality for a Trusted Dealer, Participant and Coordinator
Browse files Browse the repository at this point in the history
- Swift Trusted Dealer wrapper
- Go bindings
- swift lint
- Swift Format
  • Loading branch information
pacu committed Jul 2, 2024
1 parent 856aeb9 commit 86f3606
Show file tree
Hide file tree
Showing 22 changed files with 1,637 additions and 636 deletions.
72 changes: 72 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# By default, SwiftLint uses a set of sensible default rules you can adjust:
disabled_rules: # rule identifiers turned on by default to exclude from running
- colon
- comma
- control_statement
opt_in_rules: # some rules are turned off by default, so you need to opt-in
- empty_count # find all the available rules by running: `swiftlint rules`

# Alternatively, specify all rules explicitly by uncommenting this option:
# only_rules: # delete `disabled_rules` & `opt_in_rules` if using this
# - empty_parameters
# - vertical_whitespace

analyzer_rules: # rules run by `swiftlint analyze`
- explicit_self

# Case-sensitive paths to include during linting. Directory paths supplied on the
# command line will be ignored.
included:
- FrostSwift/Sources/FrostSwift
excluded: # case-sensitive paths to ignore during linting. Takes precedence over `included`
- Carthage
- Pods
- .build
- Sources/FrostSwiftFFI

# If true, SwiftLint will not fail if no lintable files are found.
allow_zero_lintable_files: false

# If true, SwiftLint will treat all warnings as errors.
strict: false

# The path to a baseline file, which will be used to filter out detected violations.
baseline: Baseline.json

# The path to save detected violations to as a new baseline.
write_baseline: Baseline.json

# configurable rules can be customized from this configuration file
# binary rules can set their severity level
force_cast: warning # implicitly
force_try:
severity: warning # explicitly
# rules that have both warning and error levels, can set just the warning level
# implicitly
line_length: 110
# they can set both implicitly with an array
type_body_length:
- 300 # warning
- 400 # error
# or they can set both explicitly
file_length:
warning: 500
error: 1200
# naming rules can set warnings/errors for min_length and max_length
# additionally they can set excluded names
type_name:
min_length: 4 # only warning
max_length: # warning and error
warning: 50
error: 50
excluded: iPhone # excluded via string
allowed_symbols: ["_"] # these are allowed in type names
identifier_name:
min_length: # only min_length
error: 3 # only error
excluded: # excluded via string array
- i
- id
- URL
- GlobalAPIKey
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging, summary)
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import XCTest

final class FrostCompanionTests: XCTestCase {

override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
Expand All @@ -31,5 +30,4 @@ final class FrostCompanionTests: XCTestCase {
// Put the code you want to measure the time of here.
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import XCTest

final class FrostCompanionUITests: XCTestCase {

override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import XCTest

final class FrostCompanionUITestsLaunchTests: XCTestCase {

override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}
Expand Down
274 changes: 274 additions & 0 deletions FrostSwift/Sources/FrostSwift/Coordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
//
// Coordinator.swift
//
//
// Created by Pacu on 27-06-2024.
//

import Foundation
import FrostSwiftFFI

enum FROSTCoordinatorError: Error {
case repeatedCommitmentFromIdentifier(Identifier)
case repeatedSignatureShareFromIdentifier(Identifier)
case incorrectNumberOfCommitments(min: UInt16, max: UInt16, found: UInt16)
case signingPackageAlreadyCreated
case signingPackageMissing
// mapped from FFI
case failedToCreateSigningPackage
case invalidSigningCommitment
case identifierDeserializationError
case signingPackageSerializationError
case signatureShareDeserializationError
case publicKeyPackageDeserializationError
case signatureShareAggregationFailed(message: String)
case invalidRandomizer

/// some error we don't have mapped
case otherError(CoordinationError)

init(coordinationError: CoordinationError) {
switch coordinationError {
case .FailedToCreateSigningPackage:
self = .failedToCreateSigningPackage
case .InvalidSigningCommitment:
self = .invalidSigningCommitment
case .IdentifierDeserializationError:
self = .identifierDeserializationError
case .SigningPackageSerializationError:
self = .signingPackageSerializationError
case .SignatureShareDeserializationError:
self = .signatureShareDeserializationError
case .PublicKeyPackageDeserializationError:
self = .publicKeyPackageDeserializationError
case let .SignatureShareAggregationFailed(message: message):
self = .signatureShareAggregationFailed(message: message)
case .InvalidRandomizer:
self = .invalidRandomizer
}
}
}

/// Protocol that defines the minimum functionality of a FROST signature
/// scheme Coordinator. A coordinator can be part of the signature or not. For your
/// convenience, ``NonSigningCoordinator`` and ``SigningCoordinator``
/// implementations are provided.
/// - Important: Fallback because of misbehaving participants are not considered
/// within this protocol. Implementors must define their own strategies to deal with such
/// scenarios.
/// - Note: This protocol is bound to actors because it is assumed that several
/// participants may try to interact with it concurrently.
public protocol FROSTCoordinator: Actor {
/// configuration of the FROST threshold scheme
var configuration: Configuration { get }
/// public key tha represent this signature scheme
var publicKeyPackage: PublicKeyPackage { get }
/// message to be signed by the _t_ participants
var message: Message { get }
/// this is the configuration of Round 2.
var round2Config: Round2Configuration? { get }
/// receives a signing commitment from a given participant
/// - parameter commitment: the SigningCommitment from signature participant
/// - throws: should throw ``FROSTCoordinatorError.alreadyReceivedCommitmentFromIdentifier`` when
/// receiving a commitment from an Identifier that was received already independently from whether
/// it is the same or a different one.
func receive(commitment: SigningCommitments) async throws
/// Creates a ``Round2Configuration`` struct with a ``SigningPackage`` and
/// ``Randomizer`` for the case that Re-Randomized FROST is used.
/// - throws: ``FROSTCoordinatorError.signingPackageAlreadyCreated`` if this function was
/// called already or ``FROSTCoordinatorError.incorrectNumberOfCommitments``
/// when the number of commitments gathered is less or more than the specified by the ``Configuration``
func createSigningPackage() async throws -> Round2Configuration
/// Receives a ``SignatureShare`` from a ``SigningParticipant``.
/// - throws: ``FROSTCoordinatorError.alreadyReceivedSignatureShareFromIdentifier``
/// when the same participant sends the same share repeatedly
func receive(signatureShare: SignatureShare) async throws
/// Aggregates all shares and creates the FROST ``Signature``
/// - throws: several things can go wrong here. 🥶
func aggregate() async throws -> Signature
/// Verify a ``Signature`` with the ``PublicKeyPackage`` of this coordinator.
func verify(signature: Signature) async throws
}

/// A signature scheme coordinator that does not participate in the signature scheme
public actor NonSigningCoordinator: FROSTCoordinator {
public let configuration: Configuration
public let publicKeyPackage: PublicKeyPackage
public let message: Message
public var round2Config: Round2Configuration?
var commitments: [Identifier: SigningCommitments] = [:]
var signatureShares: [Identifier: SignatureShare] = [:]

public init(configuration: Configuration, publicKeyPackage: PublicKeyPackage, message: Message) throws {
self.configuration = configuration
self.publicKeyPackage = publicKeyPackage
self.message = message
}

public func receive(commitment: SigningCommitments) throws {
// TODO: validate that the commitment belongs to a known identifier
guard commitments[commitment.identifier] == nil else {
throw FROSTCoordinatorError.repeatedCommitmentFromIdentifier(commitment.identifier)
}

commitments[commitment.identifier] = commitment
}

public func createSigningPackage() throws -> Round2Configuration {
guard round2Config?.signingPackage == nil else {
throw FROSTCoordinatorError.signingPackageAlreadyCreated
}

try validateNumberOfCommitments()

let package = try SigningPackage(
package: newSigningPackage(
message: message,
commitments: commitments.values.map { $0.commitment }
)
)

let randomizedParams = try RandomizedParams(
publicKey: publicKeyPackage,
signingPackage: package
)

let randomizer = try randomizedParams.randomizer()

let config = Round2Configuration(signingPackage: package, randomizer: randomizer)
round2Config = config

return config
}

/// receives the signature share from a partipicant
public func receive(signatureShare: SignatureShare) throws {
// TODO: validate that the commitment belongs to a known identifier
guard signatureShares[signatureShare.identifier] == nil else {
throw FROSTCoordinatorError.repeatedSignatureShareFromIdentifier(signatureShare.identifier)
}

signatureShares[signatureShare.identifier] = signatureShare
}

public func aggregate() throws -> Signature {
try validateNumberOfCommitments()
let round2config = try round2ConfigPresent()

guard let randomizer = round2config.randomizer?.randomizer else {
throw FROSTCoordinatorError.invalidRandomizer
}

let signature = try FrostSwiftFFI.aggregate(
signingPackage: round2config.signingPackage.package,
signatureShares: signatureShares.values.map { $0.share },
pubkeyPackage: publicKeyPackage.package,
randomizer: randomizer
)

return Signature(signature: signature)
}

public func verify(signature _: Signature) throws {
throw FrostError.invalidSignature
}

func round2ConfigPresent() throws -> Round2Configuration {
guard let config = round2Config else {
throw FROSTCoordinatorError.signingPackageMissing
}

return config
}

func validateNumberOfCommitments() throws {
guard commitments.count >= configuration.minSigners &&
commitments.count <= configuration.maxSigners
else {
throw FROSTCoordinatorError.incorrectNumberOfCommitments(
min: configuration.minSigners,
max: configuration.maxSigners,
found: UInt16(commitments.count)
)
}
}
}

/// A Coordinator that also participates in the signature production.
public actor SigningCoordinator: FROSTCoordinator {
public let configuration: Configuration
public let publicKeyPackage: PublicKeyPackage
public let message: Message
public var round2Config: Round2Configuration?
let nonSigningCoordinator: NonSigningCoordinator
let signingParticipant: SigningParticipant

public init(
configuration: Configuration,
publicKeyPackage: PublicKeyPackage,
signingParticipant: SigningParticipant,
message: Message
) throws {
self.configuration = configuration
self.publicKeyPackage = publicKeyPackage
self.message = message
nonSigningCoordinator = try NonSigningCoordinator(
configuration: configuration,
publicKeyPackage: publicKeyPackage,
message: message
)
self.signingParticipant = signingParticipant
}

public init(configuration: Configuration, publicKeyPackage: PublicKeyPackage, keyPackage: KeyPackage, message: Message) throws {
let signingParticipant = SigningParticipant(
keyPackage: keyPackage,
publicKey: publicKeyPackage
)

try self.init(
configuration: configuration,
publicKeyPackage: publicKeyPackage,
signingParticipant: signingParticipant,
message: message
)
}

public func receive(commitment: SigningCommitments) async throws {
try await nonSigningCoordinator.receive(commitment: commitment)
}

public func createSigningPackage() async throws -> Round2Configuration {
// sends its own commitment before creating the signign package
let commitment = try signingParticipant.commit()
try await nonSigningCoordinator.receive(commitment: commitment)

// create the signing package
let round2Config = try await nonSigningCoordinator.createSigningPackage()
self.round2Config = round2Config
return round2Config
}

public func receive(signatureShare: SignatureShare) async throws {
try await nonSigningCoordinator.receive(signatureShare: signatureShare)
}

public func aggregate() async throws -> Signature {
let round2Config = try await nonSigningCoordinator.round2ConfigPresent()
// create own signature share
signingParticipant.receive(round2Config: round2Config)
let signatureShare = try signingParticipant.sign()

// sends its own share before creating the signature
try await nonSigningCoordinator.receive(signatureShare: signatureShare)

// produce signature by aggregating all shares.
let signature = try await nonSigningCoordinator.aggregate()

return signature
}

public func verify(signature: Signature) async throws {
try await nonSigningCoordinator.verify(signature: signature)
}
}
16 changes: 16 additions & 0 deletions FrostSwift/Sources/FrostSwift/Error.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// Error.swift
//
//
// Created by Pacu in 2024.
//

import Foundation
import FrostSwiftFFI

public enum FrostError: Error {
case invalidConfiguration
case invalidSignature
case malformedIdentifier
case otherError(FrostSwiftFFI.FrostError)
}
Loading

0 comments on commit 86f3606

Please sign in to comment.