From 61a5c500af889e033b5d0731ec511a3a314b1b5b Mon Sep 17 00:00:00 2001 From: Sergei Razmetov Date: Fri, 12 Dec 2025 14:10:01 +0300 Subject: [PATCH] Fix SSH ECDSA signature mpint encoding OpenSSHSignatureWriter was emitting non-canonical mpints (keeping fixed-width leading 0x00 bytes), which breaks strict parsers. Canonicalize r/s mpints and add a regression test. --- .../OpenSSH/OpenSSHSignatureWriter.swift | 31 +++-- .../OpenSSHSignatureWriterTests.swift | 110 ++++++++++++++++++ 2 files changed, 130 insertions(+), 11 deletions(-) create mode 100644 Sources/Packages/Tests/SecretKitTests/OpenSSHSignatureWriterTests.swift diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHSignatureWriter.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHSignatureWriter.swift index b713d53..6b31de5 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHSignatureWriter.swift +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHSignatureWriter.swift @@ -29,19 +29,28 @@ public struct OpenSSHSignatureWriter: Sendable { extension OpenSSHSignatureWriter { + /// Converts a fixed-width big-endian integer (e.g. r/s from CryptoKit rawRepresentation) into an SSH mpint. + /// Strips unnecessary leading zeros and prefixes `0x00` if needed to keep the value positive. + private func mpint(fromFixedWidthPositiveBytes bytes: Data) -> Data { + // mpint zero is encoded as a string with zero bytes of data. + guard let firstNonZeroIndex = bytes.firstIndex(where: { $0 != 0x00 }) else { + return Data() + } + + let trimmed = Data(bytes[firstNonZeroIndex...]) + + if let first = trimmed.first, first >= 0x80 { + var prefixed = Data([0x00]) + prefixed.append(trimmed) + return prefixed + } + return trimmed + } + func ecdsaSignature(_ rawRepresentation: Data, keyType: KeyType) -> Data { let rawLength = rawRepresentation.count/2 - // Check if we need to pad with 0x00 to prevent certain - // ssh servers from thinking r or s is negative - let paddingRange: ClosedRange = 0x80...0xFF - var r = Data(rawRepresentation[0.. Int { + guard offset + 4 <= data.count else { throw ParseError.eof } + let value = data[offset.. Data { + guard count >= 0 else { throw ParseError.invalidLength } + guard offset + count <= data.count else { throw ParseError.eof } + let out = data[offset.. Data { + let length = try readU32() + return try readBytes(count: length) + } + } + + func parseEcdsaSignatureMpints(from openSSHSignedData: Data) throws -> (r: Data, s: Data) { + var reader = Reader(data: openSSHSignedData) + + let outerLength = try reader.readU32() + guard outerLength == (openSSHSignedData.count - 4) else { throw ParseError.invalidLength } + + let algorithm = try reader.readString() + guard String(data: algorithm, encoding: .utf8) == "ecdsa-sha2-nistp256" else { + throw ParseError.invalidAlgorithm + } + + let signatureChunk = try reader.readString() + var sigReader = Reader(data: signatureChunk) + let r = try sigReader.readString() + let s = try sigReader.readString() + return (r, s) + } + +} +