diff --git a/.spi.yml b/.spi.yml
new file mode 100644
index 0000000..e847f90
--- /dev/null
+++ b/.spi.yml
@@ -0,0 +1,4 @@
+version: 1
+builder:
+ configs:
+ - documentation_targets: [Minisign]
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Minisign.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Minisign.xcscheme
new file mode 100644
index 0000000..0d42160
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/Minisign.xcscheme
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..4f172c6
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Stephen Larew, 2025 Shibo Lyu
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Package.resolved b/Package.resolved
new file mode 100644
index 0000000..6a0d146
--- /dev/null
+++ b/Package.resolved
@@ -0,0 +1,24 @@
+{
+ "originHash" : "def130774b50dc2ba17b29adfc982eea080e276d775b67d2d3d92bd096b2287d",
+ "pins" : [
+ {
+ "identity" : "swift-blake2",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/lovetodream/swift-blake2",
+ "state" : {
+ "revision" : "b8074b87567037046445ce0859644c283dcce8cc",
+ "version" : "0.1.0"
+ }
+ },
+ {
+ "identity" : "swift-crypto",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-crypto",
+ "state" : {
+ "revision" : "60f13f60c4d093691934dc6cfdf5f508ada1f894",
+ "version" : "2.6.0"
+ }
+ }
+ ],
+ "version" : 3
+}
diff --git a/Package.swift b/Package.swift
index f805625..be13759 100644
--- a/Package.swift
+++ b/Package.swift
@@ -4,21 +4,46 @@
import PackageDescription
let package = Package(
- name: "Minisign",
- products: [
- // Products define the executables and libraries a package produces, making them visible to other packages.
- .library(
- name: "Minisign",
- targets: ["Minisign"]),
- ],
- targets: [
- // Targets are the basic building blocks of a package, defining a module or a test suite.
- // Targets can depend on other targets in this package and products from dependencies.
- .target(
- name: "Minisign"),
- .testTarget(
- name: "MinisignTests",
- dependencies: ["Minisign"]
- ),
- ]
+ name: "Minisign",
+ platforms: [
+ .macOS(.v10_15),
+ .iOS(.v13),
+ .visionOS(.v1),
+ .tvOS(.v13),
+ .watchOS(.v6)
+ ],
+ products: [
+ // Products define the executables and libraries a package produces, making them visible to other packages.
+ .library(
+ name: "Minisign",
+ targets: ["Minisign"]
+ )
+ ],
+ traits: [
+ .init(
+ name: "UseSwiftCrypto",
+ description:
+ "Use Swift Crypto instead of Apple's CryptoKit. If targeting Apple platforms only, remove this trait to cut dependency on Swift Crypto."
+ ),
+ .default(enabledTraits: ["UseSwiftCrypto"]),
+ ],
+ dependencies: [
+ .package(url: "https://github.com/apple/swift-crypto", from: "2.0.0"),
+ .package(url: "https://github.com/lovetodream/swift-blake2", from: "0.1.0")
+ ],
+ targets: [
+ // Targets are the basic building blocks of a package, defining a module or a test suite.
+ // Targets can depend on other targets in this package and products from dependencies.
+ .target(
+ name: "Minisign",
+ dependencies: [
+ .product(name: "Crypto", package: "swift-crypto", condition: .when(traits: ["UseSwiftCrypto"])),
+ .product(name: "BLAKE2", package: "swift-blake2")
+ ]
+ ),
+ .testTarget(
+ name: "MinisignTests",
+ dependencies: ["Minisign"]
+ ),
+ ]
)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5a22615
--- /dev/null
+++ b/README.md
@@ -0,0 +1,15 @@
+# Swift Minisign
+
+Swift implementation of Minisign, a simple and secure tool for signing and verifying files.
+
+This is a fork of [slarew/swift-minisign](https://github.com/slarew/swift-minisign), with these improvements:
+
+- Convenient & efficient API for verifying (big) files
+- Replaced C wrapping `swift-crypto-blake2` with [pure Swift implementation of blake2b](https://github.com/lovetodream/swift-blake2).
+- For Apple platforms, Swift Crypto dependency is now optional, controllable via trait `UseSwiftCrypto`.
+
+*but still, only signature verification is supported, signing not yet!*
+
+## License
+
+[MIT](LICENSE).
diff --git a/Sources/Minisign/Minisign.swift b/Sources/Minisign/Minisign.swift
index 08b22b8..31bca54 100644
--- a/Sources/Minisign/Minisign.swift
+++ b/Sources/Minisign/Minisign.swift
@@ -1,2 +1,157 @@
-// The Swift Programming Language
-// https://docs.swift.org/swift-book
+// SPDX-License-Identifier: MIT
+// Based on https://github.com/slarew/swift-minisign
+// Copyright 2021 Stephen Larew, 2025 Shibo Lyu
+
+import BLAKE2
+import Foundation
+
+#if UseSwiftCrypto
+ import Crypto
+#else
+ import CryptoKit
+#endif
+
+public enum SignatureAlgorithm: RawRepresentable {
+ case pureEdDSA
+ case hashedEdDSA
+
+ private static let dataEd = "Ed".data(using: .utf8)!
+ private static let dataED = "ED".data(using: .utf8)!
+
+ public init?(rawValue: Data) {
+ if rawValue == Self.dataEd {
+ self = .pureEdDSA
+ } else if rawValue == Self.dataED {
+ self = .hashedEdDSA
+ } else {
+ return nil
+ }
+ }
+
+ public var rawValue: Data {
+ switch self {
+ case .pureEdDSA: return Self.dataEd
+ case .hashedEdDSA: return Self.dataED
+ }
+ }
+}
+
+private let untrustedCommentHeader = "untrusted comment: ".data(using: .utf8)!
+private let trustedCommentHeader = "trusted comment: ".data(using: .utf8)!
+
+public struct PublicKey {
+ public let untrustedComment: String
+ public let signatureAlgorithm: SignatureAlgorithm
+ public let keyID: Data
+ public let publicKey: Curve25519.Signing.PublicKey
+
+ public init?(text: D) where D: DataProtocol {
+ let lines = text.split(separator: UInt8(ascii: "\n"), maxSplits: 2, omittingEmptySubsequences: false)
+ guard lines.count == 2 || lines[2].isEmpty else { return nil }
+ guard lines[0].starts(with: untrustedCommentHeader) else { return nil }
+ guard
+ let untrustedComment = String(
+ data: Data(lines[0].suffix(from: untrustedCommentHeader.count as! D.Index)),
+ encoding: .utf8
+ )
+ else { return nil }
+ self.untrustedComment = untrustedComment
+ guard let decLine2 = Data(base64Encoded: Data(lines[1]), options: []) else { return nil }
+ guard decLine2.count == 42 else { return nil }
+ guard let sigAlgo = SignatureAlgorithm(rawValue: decLine2.prefix(2)),
+ sigAlgo == .pureEdDSA
+ else { return nil }
+ self.signatureAlgorithm = sigAlgo
+ self.keyID = decLine2[2..<10]
+ guard let publicKey = try? Curve25519.Signing.PublicKey(rawRepresentation: decLine2[10..<42]) else {
+ return nil
+ }
+ self.publicKey = publicKey
+ }
+
+ public func isValidSignature(_ signature: Signature, for data: D) -> Bool where D: DataProtocol {
+ guard signature.keyID == keyID else { return false }
+ switch signature.signatureAlgorithm {
+ case .pureEdDSA:
+ guard publicKey.isValidSignature(signature.signature, for: data) else { return false }
+ case .hashedEdDSA:
+ let digest = try! BLAKE2b.hash(data: data) // Default parameters are guaranteed to be correct
+ guard publicKey.isValidSignature(signature.signature, for: digest) else { return false }
+ }
+ let globalData = signature.signature + signature.trustedCommentData
+ guard publicKey.isValidSignature(signature.globalSignature, for: globalData) else { return false }
+ return true
+ }
+
+ /// Verify the signature for a file using the hashed EdDSA algorithm.
+ ///
+ /// This method reads the file in chunks to avoid loading the entire file into memory, but does so in a blocking manner.
+ /// It's recommended to use this method in a background thread or task.
+ public func isValidSignature(_ signature: Signature, forFileAt url: URL) throws -> Bool {
+ guard signature.signatureAlgorithm == .hashedEdDSA else { throw SignatureVerifyError.algorithmNotSupportedForFile }
+ var blake2b = try! BLAKE2b()
+
+ let fileHandle = try FileHandle(forReadingFrom: url)
+
+ let bufferSize = 4096
+ while true {
+ let data = fileHandle.readData(ofLength: bufferSize)
+ if data.isEmpty { break }
+ blake2b.update(data: data)
+ }
+
+ defer { try? fileHandle.close() }
+
+ let digest = blake2b.finalize()
+ guard publicKey.isValidSignature(signature.signature, for: digest) else { return false }
+ let globalData = signature.signature + signature.trustedCommentData
+ guard publicKey.isValidSignature(signature.globalSignature, for: globalData) else { return false }
+ return true
+ }
+
+ public enum SignatureVerifyError: Error {
+ /// For ``PublicKey/isValidSignature(_:forFileAt:)``, only ``SignatureAlgorithm/hashedEdDSA`` algorithm is supported.
+ case algorithmNotSupportedForFile
+ }
+}
+
+public struct Signature {
+ public let untrustedComment: String
+ public let signatureAlgorithm: SignatureAlgorithm
+ public let keyID: Data
+ public let signature: Data
+ public let trustedCommentData: Data
+ public let globalSignature: Data
+
+ public var trustedComment: String? {
+ return String(data: trustedCommentData, encoding: .utf8)
+ }
+
+ public init?(text: D) where D: DataProtocol {
+ let lines = text.split(separator: UInt8(ascii: "\n"), maxSplits: 4, omittingEmptySubsequences: false)
+ guard lines.count == 4 || lines[4].isEmpty else { return nil }
+
+ guard lines[0].starts(with: untrustedCommentHeader) else { return nil }
+ guard
+ let untrustedComment = String(
+ data: Data(lines[0].suffix(from: untrustedCommentHeader.count as! D.Index)),
+ encoding: .utf8
+ )
+ else { return nil }
+ self.untrustedComment = untrustedComment
+
+ guard let decLine2 = Data(base64Encoded: Data(lines[1]), options: []) else { return nil }
+ guard decLine2.count == 74 else { return nil }
+ guard let sigAlgo = SignatureAlgorithm(rawValue: decLine2.prefix(2)) else { return nil }
+ self.signatureAlgorithm = sigAlgo
+ self.keyID = decLine2[2..<10]
+ self.signature = decLine2[10..<74]
+
+ guard lines[2].starts(with: trustedCommentHeader) else { return nil }
+ self.trustedCommentData = Data(lines[2]).suffix(from: trustedCommentHeader.count)
+
+ guard let decLine4 = Data(base64Encoded: Data(lines[3]), options: []) else { return nil }
+ guard decLine4.count == 64 else { return nil }
+ self.globalSignature = decLine4
+ }
+}
diff --git a/Tests/MinisignTests/MinisignTests.swift b/Tests/MinisignTests/MinisignTests.swift
index 2cb1ac3..f82a21d 100644
--- a/Tests/MinisignTests/MinisignTests.swift
+++ b/Tests/MinisignTests/MinisignTests.swift
@@ -1,6 +1,110 @@
-import Testing
-@testable import Minisign
+// SPDX-License-Identifier: MIT
+// Based on https://github.com/slarew/swift-minisign
+// Copyright 2021 Stephen Larew, 2025 Shibo Lyu
-@Test func example() async throws {
- // Write your test here and use APIs like `#expect(...)` to check expected conditions.
+import Foundation
+import Minisign
+import Testing
+
+struct MinisignTests {
+
+ // password: test
+ static let privKey = """
+ untrusted comment: minisign encrypted secret key
+ RWRTY0Iyvmea6pdrXYdDVqn91GknFBllkJmsQyS2jpVGoBqETB4AAAACAAAAAAAAAEAAAAAAr5jLDlb+ahHMPoPZAawLCKbUilW5ECEFsFCFSQLXfKrFQDv54sYqJzr3rR4gTmDnplQY+/T+EYCZkc5+QJOWwmaKBHPRx+Tw3rFH4CfCGkYRr4WNdZprmAzi1ZNzTl/wyvc1/uplgO8=
+
+ """
+
+ static let pubKey = """
+ untrusted comment: minisign public key E28A983382D6D7E9
+ RWTp19aCM5iK4plw14gbtviwUSISZP++TJMfOfNTKoCcRIkcrV13Oppe
+
+ """
+
+ static let signature = """
+ untrusted comment: signature from minisign secret key
+ RWTp19aCM5iK4olS02BlgllVHi3lvR9OYUVu7gM/lMsTRsO2Qb1IBxJBt3xW14hAFZo7Zlceavr7u69Rt0Wk5wMX0ShF13DZygY=
+ trusted comment: timestamp:1629695994\tfile:test.pub
+ o4E++I6KyX1h3iYMQ5yNyqEfhphdrIXiFmnWarzbB1BQpsckcO1I3LLttzS1w2CjCEauKZ3bOeY//sYui8rbAQ==
+
+ """
+
+ static let badSignature = """
+ untrusted comment: signature from minisign secret key
+ RWTp19aCM5iK4olS02BlgllVHi3lvR9OYUVu7gM/lMsTRsO2Qb1IBxJBt3xW14hAFZo7Zlceavr7u69Rt0Wk5wMX0ShF13DZygY=
+ trusted comment: timestamp:1629695994\tfile:test.pu
+ o4E++I6KyX1h3iYMQ5yNyqEfhphdrIXiFmnWarzbB1BQpsckcO1I3LLttzS1w2CjCEauKZ3bOeY//sYui8rbAQ==
+
+ """
+
+ static let prehashedSignature = """
+ untrusted comment: signature from minisign secret key
+ RUTp19aCM5iK4qzCz7Z/Y4YGsKxamuPediRB9WhvHRWnrJFREb/m9TCwxQUlug1QMYMqgaEi3IGS0trOxy4xhCkS3D7ksjLEFQg=
+ trusted comment: timestamp:1629695918\tfile:test.pub
+ 0sZUtAIqxCkdV8nQ5+bODUIX09QZS4ilrsCT6wjkTXhsMJ2cQKL0wYH3Km8ZGG46Q2OhOY8sPl+2DTLjvrMmBg==
+
+ """
+
+ static let badPrehashedSignature = """
+ untrusted comment: signature from minisign secret key
+ RUTp19aCM5iK4qzCz7Z/Y4YGsKxamuPediRB9WhvHRWnrJFREb/m9TCwxQUlug1QMYMqgaEi3IGS0trOxy4xhCkS3D7ksjLEFQg=
+ trusted comment: timestamp:1629695918\tfile:test.pu
+ 0sZUtAIqxCkdV8nQ5+bODUIX09QZS4ilrsCT6wjkTXhsMJ2cQKL0wYH3Km8ZGG46Q2OhOY8sPl+2DTLjvrMmBg==
+
+ """
+
+ @Test func parse() {
+ let pubKey = PublicKey(text: Self.pubKey.data(using: .utf8)!)
+ #expect(pubKey != nil)
+ #expect(pubKey?.untrustedComment == "minisign public key E28A983382D6D7E9")
+ #expect(pubKey?.keyID == Data(base64Encoded: "6dfWgjOYiuI=")!)
+ #expect(pubKey?.signatureAlgorithm == .pureEdDSA)
+ let sig = Signature(text: Self.signature.data(using: .utf8)!)
+ #expect(sig != nil)
+ #expect(sig?.untrustedComment == "signature from minisign secret key")
+ #expect(sig?.trustedComment == "timestamp:1629695994\tfile:test.pub")
+ #expect(sig?.signatureAlgorithm == .pureEdDSA)
+ #expect(sig?.keyID == Data(base64Encoded: "6dfWgjOYiuI=")!)
+ }
+
+ @Test func signature() {
+ let pubKey = PublicKey(text: Self.pubKey.data(using: .utf8)!)
+ #expect(pubKey != nil)
+ let sig = Signature(text: Self.signature.data(using: .utf8)!)
+ #expect(sig != nil)
+
+ #expect(pubKey!.isValidSignature(sig!, for: Self.pubKey.data(using: .utf8)!))
+ #expect(!pubKey!.isValidSignature(sig!, for: Self.pubKey.data(using: .utf8)!.advanced(by: 1)))
+
+ let badSig = Signature(text: Self.badSignature.data(using: .utf8)!)
+ #expect(badSig != nil)
+ #expect(!pubKey!.isValidSignature(badSig!, for: Self.pubKey.data(using: .utf8)!))
+ }
+
+ @Test func prehashedSignature() {
+ let pubKey = PublicKey(text: Self.pubKey.data(using: .utf8)!)
+ #expect(pubKey != nil)
+ let phSig = Signature(text: Self.prehashedSignature.data(using: .utf8)!)
+ #expect(phSig != nil)
+
+ #expect(pubKey!.isValidSignature(phSig!, for: Self.pubKey.data(using: .utf8)!))
+ #expect(!pubKey!.isValidSignature(phSig!, for: Self.pubKey.data(using: .utf8)!.advanced(by: 1)))
+
+ let badPhSig = Signature(text: Self.badPrehashedSignature.data(using: .utf8)!)
+ #expect(badPhSig != nil)
+ #expect(!pubKey!.isValidSignature(badPhSig!, for: Self.pubKey.data(using: .utf8)!))
+ }
+
+ @Test func prehashedSignatureForFile() {
+ let pubKey = PublicKey(text: Self.pubKey.data(using: .utf8)!)
+ #expect(pubKey != nil)
+ let phSig = Signature(text: Self.prehashedSignature.data(using: .utf8)!)
+ #expect(phSig != nil)
+
+ let fileURL = URL.temporaryDirectory.appending(component: "SwiftMinisignTests-\(UUID())-test.pub")
+ defer { try? FileManager.default.removeItem(at: fileURL) }
+
+ try! Self.pubKey.data(using: .utf8)!.write(to: fileURL)
+ #expect(try! pubKey!.isValidSignature(phSig!, forFileAt: fileURL))
+ }
}