Initial fork.

This commit is contained in:
Shibo Lyu 2025-04-22 16:57:31 +08:00
parent bea7edf8c4
commit e162e99e2c
8 changed files with 450 additions and 23 deletions

4
.spi.yml Normal file
View file

@ -0,0 +1,4 @@
version: 1
builder:
configs:
- documentation_targets: [Minisign]

View file

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1630"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Minisign"
BuildableName = "Minisign"
BlueprintName = "Minisign"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MinisignTests"
BuildableName = "MinisignTests"
BlueprintName = "MinisignTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Minisign"
BuildableName = "Minisign"
BlueprintName = "Minisign"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

21
LICENSE Normal file
View file

@ -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.

24
Package.resolved Normal file
View file

@ -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
}

View file

@ -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"]
),
]
)

15
README.md Normal file
View file

@ -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).

View file

@ -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?<D>(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<D>(_ 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?<D>(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
}
}

View file

@ -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))
}
}