|
| 1 | +// |
| 2 | +// P256Extensions.swift |
| 3 | +// ATCryptography |
| 4 | +// |
| 5 | +// Created by Christopher Jr Riley on 2025-05-13. |
| 6 | +// |
| 7 | + |
| 8 | +import Foundation |
| 9 | +import Crypto |
| 10 | +import BigInt |
| 11 | + |
| 12 | +extension P256.Signing.PublicKey { |
| 13 | + |
| 14 | + /// Returns the compressed SEC1 representation of a P256 public key, |
| 15 | + /// compatible with platforms where `.compressedRepresentation` is unavailable. |
| 16 | + /// |
| 17 | + /// The compressed form includes only the X coordinate and a prefix byte |
| 18 | + /// (0x02 for even Y, 0x03 for odd Y), following the SEC1 standard. |
| 19 | + /// |
| 20 | + /// - Returns: A 33-byte compressed public key. |
| 21 | + /// - Throws: An error if the raw representation is invalid or not uncompressed. |
| 22 | + @available(iOS, introduced: 13, obsoleted: 16) |
| 23 | + @available(tvOS, introduced: 13, obsoleted: 16) |
| 24 | + @available(macOS, unavailable) |
| 25 | + @available(visionOS, unavailable) |
| 26 | + @available(watchOS, unavailable) |
| 27 | + public func compressedRepresentationCompat() throws -> Data { |
| 28 | + let rawKey = self.rawRepresentation |
| 29 | + |
| 30 | + // Ensure it’s exactly 64 bytes: 32 bytes for X, 32 for Y. |
| 31 | + guard rawKey.count == 64 else { |
| 32 | + throw P256Error.invalidCompressedKey |
| 33 | + } |
| 34 | + |
| 35 | + let xCoordinate = rawKey.prefix(32) |
| 36 | + let yCoordinate = rawKey.suffix(32) |
| 37 | + |
| 38 | + guard let lastByteOfY = yCoordinate.last else { |
| 39 | + throw P256Error.invalidCompressedKey |
| 40 | + } |
| 41 | + |
| 42 | + let prefixByte: UInt8 = (lastByteOfY % 2 == 0) ? 0x02 : 0x03 |
| 43 | + return Data([prefixByte]) + xCoordinate |
| 44 | + } |
| 45 | + |
| 46 | + /// Decompresses a compressed P256 public key into a full uncompressed SEC1 key, |
| 47 | + /// and initializes a `P256.Signing.PublicKey` from it. |
| 48 | + /// |
| 49 | + /// This function is designed to support older Apple platforms (iOS/tvOS 13–15) |
| 50 | + /// where `.init(compressedRepresentation:)` is unavailable. |
| 51 | + /// |
| 52 | + /// - Parameter compressedKey: The SEC1 compressed public key data. |
| 53 | + /// - Returns: A valid `P256.Signing.PublicKey`. |
| 54 | + /// - Throws: `P256Error.invalidCompressedKey` or `P256Error.pointNotOnCurve` |
| 55 | + /// if the data is malformed or does not represent a point on the P256 curve. |
| 56 | + @available(iOS, introduced: 13, obsoleted: 16) |
| 57 | + @available(tvOS, introduced: 13, obsoleted: 16) |
| 58 | + @available(macOS, unavailable) |
| 59 | + @available(visionOS, unavailable) |
| 60 | + @available(watchOS, unavailable) |
| 61 | + public static func decompressP256PublicKey(compressed compressedKey: Data) throws -> P256.Signing.PublicKey { |
| 62 | + guard compressedKey.count == 33 else { |
| 63 | + throw P256Error.invalidCompressedKey |
| 64 | + } |
| 65 | + |
| 66 | + let prefixByte = compressedKey[0] |
| 67 | + guard prefixByte == 0x02 || prefixByte == 0x03 else { |
| 68 | + throw P256Error.invalidCompressedKey |
| 69 | + } |
| 70 | + |
| 71 | + let xBytes = compressedKey.dropFirst() |
| 72 | + let xCoordinate = BigUInt(xBytes) |
| 73 | + |
| 74 | + guard |
| 75 | + let prime = BigUInt("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", radix: 16), |
| 76 | + let b = BigUInt("5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", radix: 16) |
| 77 | + else { |
| 78 | + throw P256Error.invalidCompressedKey |
| 79 | + } |
| 80 | + |
| 81 | + let a = prime - 3 |
| 82 | + let ySquared = (xCoordinate.power(3, modulus: prime) + a * xCoordinate + b) % prime |
| 83 | + |
| 84 | + guard let yCoordinate = try modularSquareRoot(ySquared, prime: prime) else { |
| 85 | + throw P256Error.pointNotOnCurve |
| 86 | + } |
| 87 | + |
| 88 | + let isYOdd = yCoordinate % 2 == 1 |
| 89 | + let shouldBeOdd = (prefixByte == 0x03) |
| 90 | + let finalYCoordinate = (isYOdd == shouldBeOdd) ? yCoordinate : (prime - yCoordinate) |
| 91 | + |
| 92 | + let xData = xCoordinate.serialize().pad(to: 32) |
| 93 | + let yData = finalYCoordinate.serialize().pad(to: 32) |
| 94 | + |
| 95 | + let uncompressedKey = xData + yData |
| 96 | + return try P256.Signing.PublicKey(rawRepresentation: uncompressedKey) |
| 97 | + } |
| 98 | + |
| 99 | + /// Computes the modular square root of a given number (`squareRoot`) modulo a prime (`prime`). |
| 100 | + /// |
| 101 | + /// This function is specifically optimized for the p256 curve, whose `prime` satisfies the condition |
| 102 | + /// `prime ≡ 3 mod 4`. This allows the use of a simplified square root computation: |
| 103 | + /// |
| 104 | + /// sqrt(squareRoot) ≡ squareRoot^((prime + 1) / 4) mod prime |
| 105 | + /// |
| 106 | + /// This equation is valid only when `squareRoot` is a quadratic residue modulo `prime`. If `squareRoot` |
| 107 | + /// is not a square modulo `prime`, the function returns `nil`. |
| 108 | + /// |
| 109 | + /// - Parameters: |
| 110 | + /// - squareRoot: The value whose modular square root is to be computed. |
| 111 | + /// - prime: A prime modulus. For p256, this should be the curve's prime field. |
| 112 | + /// - Returns: The modular square root of `squareRoot` modulo `prime`, if it exists. |
| 113 | + /// |
| 114 | + /// - Throws: `P256Error.pointNotOnCurve` if the prime does not satisfy `prime ≡ 3 mod 4`, which means |
| 115 | + /// the simplified square root algorithm cannot be used. |
| 116 | + @available(iOS, introduced: 13, obsoleted: 16) |
| 117 | + @available(tvOS, introduced: 13, obsoleted: 16) |
| 118 | + @available(macOS, unavailable) |
| 119 | + @available(visionOS, unavailable) |
| 120 | + @available(watchOS, unavailable) |
| 121 | + private static func modularSquareRoot(_ squareRoot: BigUInt, prime prime: BigUInt) throws -> BigUInt? { |
| 122 | + // Special case for p256 where prime ≡ 3 mod 4: |
| 123 | + // sqrt(squareRoot) ≡ squareRoot^((prime + 1) / 4) mod prime |
| 124 | + if prime % 4 == 3 { |
| 125 | + let exponent = (prime + 1) / 4 |
| 126 | + let result = squareRoot.power(exponent, modulus: prime) |
| 127 | + if (result.power(2, modulus: prime) == squareRoot % prime) { |
| 128 | + return result |
| 129 | + } else { |
| 130 | + return nil |
| 131 | + } |
| 132 | + } |
| 133 | + |
| 134 | + // Otherwise, a full Tonelli-Shanks is needed. |
| 135 | + throw P256Error.pointNotOnCurve |
| 136 | + } |
| 137 | +} |
| 138 | + |
| 139 | +extension Data { |
| 140 | + |
| 141 | + /// Pads the current `Data` instance with leading zeroes to match the specified length. |
| 142 | + /// |
| 143 | + /// This is commonly used to ensure big-endian encoded integers or coordinates are a fixed size, |
| 144 | + /// such as 32 bytes for P256 public key components. |
| 145 | + /// |
| 146 | + /// - Parameter length: The target length in bytes. |
| 147 | + /// - Returns: A new `Data` instance of exactly `length` bytes, with leading zeroes added if necessary. |
| 148 | + /// If the current length is already `>= length`, the original data is returned unchanged. |
| 149 | + @available(iOS, introduced: 13, obsoleted: 16) |
| 150 | + @available(tvOS, introduced: 13, obsoleted: 16) |
| 151 | + @available(macOS, unavailable) |
| 152 | + @available(visionOS, unavailable) |
| 153 | + @available(watchOS, unavailable) |
| 154 | + public func pad(to length: Int) -> Data { |
| 155 | + if count >= length { return self } |
| 156 | + return Data(repeating: 0, count: length - count) + self |
| 157 | + } |
| 158 | +} |
| 159 | + |
| 160 | +/// Utility for compressing and decompressing P256 public keys on platforms |
| 161 | +/// where native CryptoKit support for compressed keys is unavailable. |
| 162 | +/// |
| 163 | +/// This wrapper supports SEC1 compressed key encoding (33 bytes) and |
| 164 | +/// decoding by reconstructing the full point on the curve using the |
| 165 | +/// Weierstrass equation. |
| 166 | +/// |
| 167 | +/// Use this only on iOS/tvOS 13–15. Prefer native CryptoKit APIs |
| 168 | +/// on newer platforms. |
| 169 | +@available(iOS, introduced: 13, obsoleted: 16) |
| 170 | +@available(tvOS, introduced: 13, obsoleted: 16) |
| 171 | +@available(macOS, unavailable) |
| 172 | +@available(visionOS, unavailable) |
| 173 | +@available(watchOS, unavailable) |
| 174 | +public struct CompressedP256 { |
| 175 | + |
| 176 | + /// Compresses a P256 public key using SEC1 encoding. |
| 177 | + /// |
| 178 | + /// - Parameter key: A valid uncompressed P256 public key. |
| 179 | + /// - Returns: A 33-byte compressed SEC1 representation. |
| 180 | + /// |
| 181 | + /// - Throws: If compression fails (e.g., invalid raw data). |
| 182 | + public static func compress(_ key: P256.Signing.PublicKey) throws -> Data { |
| 183 | + return try key.compressedRepresentationCompat() |
| 184 | + } |
| 185 | + |
| 186 | + /// Decompresses a SEC1 compressed public key into a usable P256 public key. |
| 187 | + /// |
| 188 | + /// - Parameter data: A 33-byte compressed key. |
| 189 | + /// - Returns: A full `P256.Signing.PublicKey`. |
| 190 | + /// - Throws: `P256Error` if the key is malformed or cannot be decompressed. |
| 191 | + public static func decompress(_ data: Data) throws -> P256.Signing.PublicKey { |
| 192 | + return try P256.Signing.PublicKey.decompressP256PublicKey(compressed: data) |
| 193 | + } |
| 194 | +} |
| 195 | + |
| 196 | +/// Errors that may occur while working with compressed P256 keys. |
| 197 | +@available(iOS, introduced: 13, obsoleted: 16) |
| 198 | +@available(tvOS, introduced: 13, obsoleted: 16) |
| 199 | +@available(macOS, unavailable) |
| 200 | +@available(visionOS, unavailable) |
| 201 | +@available(watchOS, unavailable) |
| 202 | +public enum P256Error: Error { |
| 203 | + |
| 204 | + /// The input data is not a valid compressed P256 key. |
| 205 | + case invalidCompressedKey |
| 206 | + |
| 207 | + /// The calculated Y coordinate is not a valid point on the curve. |
| 208 | + case pointNotOnCurve |
| 209 | +} |
| 210 | + |
0 commit comments