From bee2b483278d9cc6b2f96665de6ebc64b624430e Mon Sep 17 00:00:00 2001 From: Qiwei Yang Date: Wed, 3 Jul 2024 11:27:08 +0800 Subject: [PATCH] add bls (#29) * add blst work * fix missing submodule * fix * bls struct * fix * fix * fix * let pub key * swift testing * try 0.10.0 --- .github/workflows/ci.yml | 2 + .gitmodules | 3 + Makefile | 8 +- README.md | 2 + Utils/Package.resolved | 20 ++- Utils/Package.swift | 22 ++- Utils/Sources/Utils/BLS.swift | 161 ++++++++++++++++++++++ Utils/Sources/Utils/ConstValue.swift | 6 + Utils/Sources/Utils/Extensions.swift | 5 +- Utils/Sources/Utils/FixedSizeData.swift | 1 + Utils/Sources/blst | 1 + Utils/Tests/UtilsTests/BLSTests.swift | 55 ++++++++ Utils/Tests/UtilsTests/Blake2Tests.swift | 13 +- Utils/Tests/UtilsTests/Data32Tests.swift | 29 ++-- Utils/Tests/UtilsTests/Ed25519Tests.swift | 19 ++- scripts/deps.sh | 22 +++ 16 files changed, 339 insertions(+), 30 deletions(-) create mode 100644 .gitmodules create mode 100644 Utils/Sources/Utils/BLS.swift create mode 160000 Utils/Sources/blst create mode 100644 Utils/Tests/UtilsTests/BLSTests.swift create mode 100755 scripts/deps.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 772a6883..b301c76c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,8 @@ jobs: steps: - name: Checkout Code uses: actions/checkout@v4 + with: + submodules: recursive - uses: actions/cache@v4 with: path: '**/.build' diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..2043a92c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "blst"] + path = Utils/Sources/blst + url = https://github.com/supranational/blst.git diff --git a/Makefile b/Makefile index c78763cd..2a60642f 100644 --- a/Makefile +++ b/Makefile @@ -4,12 +4,16 @@ .PHONY: githooks githooks: .git/hooks/pre-commit +.PHONY: deps +deps: githooks + ./scripts/deps.sh + .PHONY: test -test: githooks +test: githooks deps ./scripts/run.sh test .PHONY: build -build: githooks +build: githooks deps ./scripts/run.sh build .PHONY: clean diff --git a/README.md b/README.md index 0d2fafe6..fdf0afab 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ JAM built with Swift - SwiftLint: `brew install swiftlint` - SwiftFormat: `brew install swiftformat` - Precommit hooks: `make githooks` +- Pull submodules: `git submodule update --init --recursive` +- Setup deps: `make deps` ## Packages diff --git a/Utils/Package.resolved b/Utils/Package.resolved index 37ec4736..15301e5d 100644 --- a/Utils/Package.resolved +++ b/Utils/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "7f9d01d4c8c5ccb64f9b9f466f7f8e61b0fd8cd435aa5c4e587e5a6e459f5d77", + "originHash" : "e52d41427ac86e2fe9044bdaf7129a42c2be6dd8550156992e8b20f787f1cc2e", "pins" : [ { "identity" : "blake2.swift", @@ -28,6 +28,24 @@ "version" : "3.4.0" } }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c", + "version" : "600.0.0-prerelease-2024-06-12" + } + }, + { + "identity" : "swift-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-testing.git", + "state" : { + "branch" : "0.10.0", + "revision" : "69d59cfc76e5daf498ca61f5af409f594768eef9" + } + }, { "identity" : "tuples.swift", "kind" : "remoteSourceControl", diff --git a/Utils/Package.swift b/Utils/Package.swift index 81218188..d035da27 100644 --- a/Utils/Package.swift +++ b/Utils/Package.swift @@ -19,6 +19,7 @@ let package = Package( .package(url: "https://github.com/tesseract-one/ScaleCodec.swift.git", from: "0.3.0"), .package(url: "https://github.com/tesseract-one/Blake2.swift.git", from: "0.2.0"), .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "4.0.0"), + .package(url: "https://github.com/apple/swift-testing.git", branch: "0.10.0"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -29,11 +30,30 @@ let package = Package( .product(name: "ScaleCodec", package: "ScaleCodec.swift"), .product(name: "Blake2", package: "Blake2.swift"), .product(name: "Crypto", package: "swift-crypto"), + "blst", + ] + ), + .target( + name: "blst", + dependencies: [], + path: "./Sources/blst", + exclude: [ + ".github", + "./build", + "./src", + ], + sources: [], + publicHeadersPath: "./include", + cSettings: [ + .headerSearchPath("./include"), + ], + linkerSettings: [ + .unsafeFlags(["-L../Utils/Sources/blst/lib", "-lblst"]), ] ), .testTarget( name: "UtilsTests", - dependencies: ["Utils"] + dependencies: ["Utils", .product(name: "Testing", package: "swift-testing")] ), ], swiftLanguageVersions: [.version("6")] diff --git a/Utils/Sources/Utils/BLS.swift b/Utils/Sources/Utils/BLS.swift new file mode 100644 index 00000000..94ebd206 --- /dev/null +++ b/Utils/Sources/Utils/BLS.swift @@ -0,0 +1,161 @@ +import blst +import Foundation + +enum BLSError: Error { + case ikmTooShort + case blstError(BLST_ERROR) +} + +/// A wrapper to blst C library. +/// +/// `blst_p1` for public keys, and `blst_p2` for signatures +public struct BLS { + public let secretKey: Data32 + + public let publicKey: Data48 + + /// Initiate a BLS secret key with IKM. + /// IKM MUST be infeasible to guess, e.g., generated by a trusted source of randomness. + /// IKM MUST be at least 32 bytes long, but it MAY be longer. + public init(ikm: Data) throws { + guard ikm.count >= 32 else { + throw BLSError.ikmTooShort + } + + var sk = blst_scalar() + let ikmBytes = [UInt8](ikm) + let ikmLen = ikmBytes.count + + blst_keygen(&sk, ikmBytes, ikmLen, nil, 0) + + var out = [UInt8](repeating: 0, count: 32) + blst_bendian_from_scalar(&out, &sk) + + secretKey = Data32(Data(out))! + publicKey = BLS.getPublicKey(secretKey) + } + + public init(privateKey: Data32) throws { + var sk = blst_scalar() + blst_scalar_from_bendian(&sk, [UInt8](privateKey.data)) + + guard blst_sk_check(&sk) else { + throw BLSError.blstError(BLST_BAD_SCALAR) + } + + secretKey = privateKey + publicKey = BLS.getPublicKey(secretKey) + } + + private static func getPublicKey(_ secretKey: Data32) -> Data48 { + var sk = blst_scalar() + blst_scalar_from_bendian(&sk, [UInt8](secretKey.data)) + + var pk = blst_p1() + blst_sk_to_pk_in_g1(&pk, &sk) + + var pkBytes = [UInt8](repeating: 0, count: 48) + blst_p1_compress(&pkBytes, &pk) + + return Data48(Data(pkBytes))! + } + + public func sign(message: Data) -> Data96 { + var sk = blst_scalar() + blst_scalar_from_bendian(&sk, [UInt8](secretKey.data)) + + var msgHash = blst_p2() + blst_hash_to_g2(&msgHash, [UInt8](message), message.count, nil, 0, nil, 0) + + var sig = blst_p2() + blst_sign_pk_in_g1(&sig, &msgHash, &sk) + + var sigBytes = [UInt8](repeating: 0, count: 96) + blst_p2_compress(&sigBytes, &sig) + + return Data96(Data(sigBytes))! + } + + public static func verify(signature: Data96, message: Data, publicKey: Data48) -> Bool { + var pk = blst_p1_affine() + var sig = blst_p2_affine() + + let pkResult = blst_p1_uncompress(&pk, [UInt8](publicKey.data)) + let sigResult = blst_p2_uncompress(&sig, [UInt8](signature.data)) + + guard pkResult == BLST_SUCCESS, sigResult == BLST_SUCCESS else { + return false + } + + let verifyResult = blst_core_verify_pk_in_g1( + &pk, &sig, true, [UInt8](message), message.count, nil, 0, nil, 0 + ) + + return verifyResult == BLST_SUCCESS + } + + public static func aggregateVerify( + signature: Data96, messages: [Data], publicKeys: [Data48] + ) + -> Bool + { + let size = blst_pairing_sizeof() + let ctx = OpaquePointer(malloc(size)) + + blst_pairing_init(ctx, true, nil, 0) + + var sig = blst_p2_affine() + let sigResult = blst_p2_uncompress(&sig, [UInt8](signature.data)) + guard sigResult == BLST_SUCCESS else { + return false + } + + for i in 0 ..< publicKeys.count { + var pk = blst_p1_affine() + let pkResult = blst_p1_uncompress(&pk, [UInt8](publicKeys[i].data)) + guard pkResult == BLST_SUCCESS else { + return false + } + + let aggregateResult: BLST_ERROR = + if i == 1 { + blst_pairing_aggregate_pk_in_g1( + ctx, &pk, &sig, [UInt8](messages[i]), messages[i].count, nil, 0 + ) + } else { + blst_pairing_aggregate_pk_in_g1( + ctx, &pk, nil, [UInt8](messages[i]), messages[i].count, nil, 0 + ) + } + guard aggregateResult == BLST_SUCCESS else { + return false + } + } + + blst_pairing_commit(ctx) + let result = blst_pairing_finalverify(ctx, nil) + + free(UnsafeMutableRawPointer(ctx)) + + return result + } + + public static func aggregateSignatures(signatures: [Data96]) throws -> Data96 { + var aggregate = blst_p2() + + for signature in signatures { + var sig = blst_p2_affine() + let sigResult = blst_p2_uncompress(&sig, [UInt8](signature.data)) + guard sigResult == BLST_SUCCESS else { + throw BLSError.blstError(sigResult) + } + var aggCopy = aggregate + blst_p2_add_or_double_affine(&aggregate, &aggCopy, &sig) + } + + var sigCompressed = [UInt8](repeating: 0, count: 96) + blst_p2_compress(&sigCompressed, &aggregate) + + return Data96(Data(sigCompressed))! + } +} diff --git a/Utils/Sources/Utils/ConstValue.swift b/Utils/Sources/Utils/ConstValue.swift index fc30c9a2..558feec6 100644 --- a/Utils/Sources/Utils/ConstValue.swift +++ b/Utils/Sources/Utils/ConstValue.swift @@ -43,6 +43,12 @@ public enum ConstInt32: ConstInt { } } +public enum ConstInt48: ConstInt { + public static var value: Int { + 48 + } +} + public enum ConstInt64: ConstInt { public static var value: Int { 64 diff --git a/Utils/Sources/Utils/Extensions.swift b/Utils/Sources/Utils/Extensions.swift index f26f1a91..a8340d33 100644 --- a/Utils/Sources/Utils/Extensions.swift +++ b/Utils/Sources/Utils/Extensions.swift @@ -10,8 +10,9 @@ extension Data { var index = hexString.startIndex while index < hexString.endIndex { - guard let nextIndex = hexString.index(index, offsetBy: 2, limitedBy: hexString.endIndex), - let byte = UInt8(hexString[index ..< nextIndex], radix: 16) + guard + let nextIndex = hexString.index(index, offsetBy: 2, limitedBy: hexString.endIndex), + let byte = UInt8(hexString[index ..< nextIndex], radix: 16) else { return nil } diff --git a/Utils/Sources/Utils/FixedSizeData.swift b/Utils/Sources/Utils/FixedSizeData.swift index bdf99dd3..be47d558 100644 --- a/Utils/Sources/Utils/FixedSizeData.swift +++ b/Utils/Sources/Utils/FixedSizeData.swift @@ -39,6 +39,7 @@ extension FixedSizeData: ScaleCodec.Codable { } public typealias Data32 = FixedSizeData +public typealias Data48 = FixedSizeData public typealias Data64 = FixedSizeData public typealias Data96 = FixedSizeData public typealias Data128 = FixedSizeData diff --git a/Utils/Sources/blst b/Utils/Sources/blst new file mode 160000 index 00000000..e99f7db0 --- /dev/null +++ b/Utils/Sources/blst @@ -0,0 +1 @@ +Subproject commit e99f7db0db413e2efefcfd077a4e335766f39c27 diff --git a/Utils/Tests/UtilsTests/BLSTests.swift b/Utils/Tests/UtilsTests/BLSTests.swift new file mode 100644 index 00000000..b3f8f1ea --- /dev/null +++ b/Utils/Tests/UtilsTests/BLSTests.swift @@ -0,0 +1,55 @@ +import Foundation +import Testing + +@testable import Utils + +@Suite struct BLSTests { + @Test func BLSSignatureWorks() throws { + let bls = try BLS(ikm: Data("this is random high entropy ikm for key1".utf8)) + let publicKey1 = bls.publicKey + let message1 = Data("test1".utf8) + let signature1 = bls.sign(message: message1) + + #expect( + BLS.verify(signature: signature1, message: message1, publicKey: publicKey1) + ) + + let invalidMessage = Data("testUnknown".utf8) + #expect( + !BLS.verify(signature: signature1, message: invalidMessage, publicKey: publicKey1) + ) + + var invalidSignature = signature1.data + invalidSignature.replaceSubrange(0 ... 1, with: [2, 3]) + #expect( + !BLS.verify( + signature: Data96(invalidSignature)!, message: message1, publicKey: publicKey1 + ) + ) + + let bls2 = try BLS(ikm: Data("this is random high entropy ikm for key2".utf8)) + let publicKey2 = bls2.publicKey + let message2 = Data("test2".utf8) + let signature2 = bls2.sign(message: message2) + + #expect( + BLS.verify(signature: signature2, message: message2, publicKey: publicKey2) + ) + + let aggSig = try BLS.aggregateSignatures(signatures: [signature1, signature2]) + + #expect( + BLS.aggregateVerify( + signature: aggSig, messages: [message1, message2], + publicKeys: [publicKey1, publicKey2] + ) + ) + + #expect( + !BLS.aggregateVerify( + signature: aggSig, messages: [message1, message2], + publicKeys: [publicKey2, publicKey1] + ) + ) + } +} diff --git a/Utils/Tests/UtilsTests/Blake2Tests.swift b/Utils/Tests/UtilsTests/Blake2Tests.swift index ab925efc..757820b0 100644 --- a/Utils/Tests/UtilsTests/Blake2Tests.swift +++ b/Utils/Tests/UtilsTests/Blake2Tests.swift @@ -1,12 +1,15 @@ -import XCTest +import Foundation +import Testing @testable import Utils -final class Blake2Tests: XCTestCase { - func testBlake2b256Test() throws { +@Suite struct Blake2Tests { + @Test func blake2b256Works() throws { let testData = Data("test".utf8) - let expected = Data(fromHexString: "928b20366943e2afd11ebc0eae2e53a93bf177a4fcf35bcc64d503704e65e202") + let expected = Data( + fromHexString: "928b20366943e2afd11ebc0eae2e53a93bf177a4fcf35bcc64d503704e65e202" + ) let actual = try blake2b256(testData) - XCTAssertEqual(expected, actual.data) + #expect(expected == actual.data) } } diff --git a/Utils/Tests/UtilsTests/Data32Tests.swift b/Utils/Tests/UtilsTests/Data32Tests.swift index 06dc1d42..deb1daea 100644 --- a/Utils/Tests/UtilsTests/Data32Tests.swift +++ b/Utils/Tests/UtilsTests/Data32Tests.swift @@ -1,30 +1,33 @@ -import XCTest +import Foundation +import Testing @testable import Utils -final class Data32Tests: XCTestCase { - func testZero() throws { +@Suite struct Data32Tests { + @Test func testZero() throws { let value = Data32() - XCTAssertEqual(value.data, Data(repeating: 0, count: 32)) - XCTAssertEqual( - value.description, "0x0000000000000000000000000000000000000000000000000000000000000000" + #expect(value.data == Data(repeating: 0, count: 32)) + #expect( + value.description + == "0x0000000000000000000000000000000000000000000000000000000000000000" ) } - func testInitWithData() throws { + @Test func testInitWithData() throws { var data = Data(repeating: 0, count: 32) for i in 0 ..< 32 { data[i] = UInt8(i) } let value = Data32(data)! - XCTAssertEqual(value.data, data) - XCTAssertEqual( - value.description, "0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + #expect(value.data == data) + #expect( + value.description + == "0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" ) } - func testInitWithInvalidData() throws { - XCTAssertNil(Data32(Data(repeating: 0, count: 31))) - XCTAssertNil(Data32(Data(repeating: 0, count: 33))) + @Test func testInitWithInvalidData() throws { + #expect(Data32(Data(repeating: 0, count: 31)) == nil) + #expect(Data32(Data(repeating: 0, count: 33)) == nil) } } diff --git a/Utils/Tests/UtilsTests/Ed25519Tests.swift b/Utils/Tests/UtilsTests/Ed25519Tests.swift index 37057745..9035feab 100644 --- a/Utils/Tests/UtilsTests/Ed25519Tests.swift +++ b/Utils/Tests/UtilsTests/Ed25519Tests.swift @@ -1,21 +1,28 @@ -import XCTest +import Foundation +import Testing @testable import Utils -final class Ed25519Tests: XCTestCase { - func testEd25519Signature() throws { +@Suite struct Ed25519Tests { + @Test func testEd25519Signature() throws { let ed25519 = Ed25519() let publicKey = ed25519.publicKey let message = Data("test".utf8) let signature = try ed25519.sign(message: message) - XCTAssertTrue(ed25519.verify(signature: signature, message: message, publicKey: publicKey)) + #expect(ed25519.verify(signature: signature, message: message, publicKey: publicKey)) let invalidMessage = Data("tests".utf8) - XCTAssertFalse(ed25519.verify(signature: signature, message: invalidMessage, publicKey: publicKey)) + #expect( + !ed25519.verify(signature: signature, message: invalidMessage, publicKey: publicKey) + ) var invalidSignature = signature.data invalidSignature.replaceSubrange(0 ... 1, with: [10, 12]) - XCTAssertFalse(ed25519.verify(signature: Data64(invalidSignature)!, message: message, publicKey: publicKey)) + #expect( + !ed25519.verify( + signature: Data64(invalidSignature)!, message: message, publicKey: publicKey + ) + ) } } diff --git a/scripts/deps.sh b/scripts/deps.sh new file mode 100755 index 00000000..e2a6d9fd --- /dev/null +++ b/scripts/deps.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + + +# Setup blst C module +cd Utils/Sources/blst || { echo "Submodule directory not found"; exit 1; } + +./build.sh || { echo "Build blst library failed"; exit 1; } + +mkdir -p include +mkdir -p lib + +cp libblst.a lib/ + +cat < include/module.modulemap +module blst { + header "../bindings/blst.h" + link "blst" + export * +} +EOL + +echo "Setup blst successfully."