From e162e99e2c5cfb49aabe0594ff299fd4b5ef2601 Mon Sep 17 00:00:00 2001 From: Shibo Lyu Date: Tue, 22 Apr 2025 16:57:31 +0800 Subject: [PATCH] Initial fork. --- .spi.yml | 4 + .../xcshareddata/xcschemes/Minisign.xcscheme | 79 +++++++++ LICENSE | 21 +++ Package.resolved | 24 +++ Package.swift | 59 +++++-- README.md | 15 ++ Sources/Minisign/Minisign.swift | 159 +++++++++++++++++- Tests/MinisignTests/MinisignTests.swift | 112 +++++++++++- 8 files changed, 450 insertions(+), 23 deletions(-) create mode 100644 .spi.yml create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/Minisign.xcscheme create mode 100644 LICENSE create mode 100644 Package.resolved create mode 100644 README.md 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)) + } }