Skip to content

Commit d123d3f

Browse files
committed
Create extensions for older iOS and tvOS versions
The methods and initializers for getting the compressed representation of p256 operations are only available for iOS 16 and tvOS 16. These extensions allows for iOS and tvOS versions lower than 16 to still use p256 features.
1 parent a56bb2b commit d123d3f

File tree

3 files changed

+248
-14
lines changed

3 files changed

+248
-14
lines changed
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
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+

Sources/ATCryptography/p256/P256Encoding.swift

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ public struct P256Encoding {
4141

4242
// Convert to a compressed public key.
4343
let key = try P256.Signing.PublicKey(rawRepresentation: Data(rawKey))
44-
return Array(key.compressedRepresentation)
44+
if #available(iOS 16, tvOS 16, *) {
45+
return Array(key.compressedRepresentation)
46+
} else {
47+
return Array(try key.compressedRepresentationCompat())
48+
}
4549
}
4650

4751
/// Decompresses a compressed p256 public key.
@@ -59,38 +63,46 @@ public struct P256Encoding {
5963
/// - Throws: `EllipticalCurveEncodingError.invalidKeyLength` if the key length is incorrect.
6064
/// `EllipticalCurveEncodingError.keyDecodingFailed` if the key decoding failed.
6165
public static func decompress(publicKey: [UInt8], shouldAddPrefix: Bool = false) throws -> [UInt8] {
62-
let rawKey: Data
66+
let compressedPublicKey: Data
6367

6468
switch publicKey.count {
6569
case 33 where publicKey.first == 0x02 || publicKey.first == 0x03:
6670
// Remove prefix before using CryptoKit.
67-
rawKey = Data(publicKey)
71+
compressedPublicKey = Data(publicKey)
6872

6973
case 32:
7074
// Already in raw format.
71-
rawKey = Data(publicKey)
75+
compressedPublicKey = Data(publicKey)
7276

7377
default:
7478
throw EllipticalCurveEncodingError.invalidKeyLength(expected: 33, actual: publicKey.count)
7579
}
7680

7781
// Convert to an uncompressed public key.
78-
let key = try P256.Signing.PublicKey(compressedRepresentation: rawKey)
79-
var uncompressedKey = Array(key.rawRepresentation)
82+
var uncompressedRawPublicKey: [Data.Element]
83+
if #available(iOS 16.0, *) {
84+
let key = try P256.Signing.PublicKey(compressedRepresentation: compressedPublicKey)
85+
uncompressedRawPublicKey = Array(key.rawRepresentation)
86+
} else {
87+
// Fallback on earlier versions
88+
let key = try CompressedP256.decompress(compressedPublicKey)
89+
uncompressedRawPublicKey = Array(key.rawRepresentation)
90+
}
91+
// var uncompressedRawPublicKey = Array(key.rawRepresentation)
8092

8193
// Prepend the uncompressed prefix (0x04)
8294
if shouldAddPrefix {
8395
// Prepend the uncompressed key prefix (0x04) if not already present.
84-
if uncompressedKey.first != 0x04 {
85-
uncompressedKey.insert(0x04, at: 0)
96+
if uncompressedRawPublicKey.first != 0x04 {
97+
uncompressedRawPublicKey.insert(0x04, at: 0)
8698
}
8799
} else {
88100
// Ensure the key is returned without the prefix.
89-
if uncompressedKey.first == 0x04 && (uncompressedKey.count - 1) != 63 {
90-
uncompressedKey.removeFirst()
101+
if uncompressedRawPublicKey.first == 0x04 && (uncompressedRawPublicKey.count - 1) != 63 {
102+
uncompressedRawPublicKey.removeFirst()
91103
}
92104
}
93105

94-
return uncompressedKey
106+
return uncompressedRawPublicKey
95107
}
96108
}

Sources/ATCryptography/p256/P256Operations.swift

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,20 @@ public struct P256Operations {
4747
let allowMalleable = options?.areMalleableSignaturesAllowed ?? false
4848
let hashedData = await SHA256Hasher.sha256(data)
4949

50-
guard let publicKey = try? P256.Signing.PublicKey(compressedRepresentation: publicKey) else {
51-
throw EllipticalCurveOperationsError.invalidPublicKey
50+
let uncompressedPublicKey: P256.Signing.PublicKey
51+
52+
if #available(iOS 16, tvOS 16, *) {
53+
guard let key = try? P256.Signing.PublicKey(compressedRepresentation: publicKey) else {
54+
throw EllipticalCurveOperationsError.invalidPublicKey
55+
}
56+
57+
uncompressedPublicKey = key
58+
} else {
59+
guard let key = try? CompressedP256.decompress(Data(publicKey)) else {
60+
throw EllipticalCurveOperationsError.invalidPublicKey
61+
}
62+
63+
uncompressedPublicKey = key
5264
}
5365

5466
let signatureData = Data(signature)
@@ -66,7 +78,7 @@ public struct P256Operations {
6678
return false
6779
}
6880

69-
return publicKey.isValidSignature(correctedSignature, for: Data(hashedData))
81+
return uncompressedPublicKey.isValidSignature(correctedSignature, for: Data(hashedData))
7082
}
7183

7284
/// Checks if a signature is in compact format.

0 commit comments

Comments
 (0)