diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index d601270..a7b4049 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -1,15 +1,21 @@ name: Swift -on: [push] +on: + push: + branches: + - main + pull_request: + jobs: units: name: Unit Tests strategy: + fail-fast: false matrix: - os: [macos-latest, macos-11, macos-10.15] + os: [macos-latest, macos-12, macos-11, macos-10.15] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Run Tests run: swift test @@ -17,7 +23,7 @@ jobs: name: Check Test Vectors Up To Date runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Check for updates run: | cd Tests/PasetoTests/TestVectors diff --git a/Sources/Paseto/Implementations/Version3/Public/V3PublicAsymmetricPublicKey.swift b/Sources/Paseto/Implementations/Version3/Public/V3PublicAsymmetricPublicKey.swift index dd301f7..e1b8cb1 100644 --- a/Sources/Paseto/Implementations/Version3/Public/V3PublicAsymmetricPublicKey.swift +++ b/Sources/Paseto/Implementations/Version3/Public/V3PublicAsymmetricPublicKey.swift @@ -1,5 +1,26 @@ import Foundation import CryptoKit +import CryptoSwift + +fileprivate let zeroBn = BigUInteger(0) +fileprivate let oneBn = BigUInteger(1) +fileprivate let twoBn = BigUInteger(2) +fileprivate let fourBn = BigUInteger(4) + +// The following consts are extracted from the NIST spec. +// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-186-draft.pdf + +// p = 2^384 − 2^128 − 2^96 + 2^32 − 1 +fileprivate let pBn = twoBn.power(384) - twoBn.power(128) - twoBn.power(96) + twoBn.power(32) - 1 + +// a = -3 mod p +// = 3940200619639447921227904010014361380507973927046544666794\ +// 8293404245721771496870329047266088258938001861606973112316 +fileprivate let aBn = BigUInteger("39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112316", radix: 10)! + +// b = 2758019355995970587784901184038904809305690585636156852142\ +// 8707301988689241309860865136260764883745107765439761230575 +fileprivate let bBn = BigUInteger("27580193559959705877849011840389048093056905856361568521428707301988689241309860865136260764883745107765439761230575", radix: 10)! @available(macOS 11, iOS 14, watchOS 7, tvOS 14, macCatalyst 14, *) extension Version3.Public { @@ -8,7 +29,74 @@ extension Version3.Public { let key: P384.Signing.PublicKey - var compressed: Bytes { + // https://www.secg.org/sec1-v2.pdf section 2.3.4, supporting only action 2. for compressed points + fileprivate static func decompressToCoords(compressedKey: Bytes) throws -> (x: Bytes, y: Bytes) { + // precondition for 2. + guard compressedKey.count == 1 + 48 else { + throw Exception.badKey("Bad public key length") + } + + // 2.1 + let prefix = compressedKey[0] + let x = compressedKey[1..<49].bytes + + // 2.2 + let xBn = BigUInteger(Data(x)) + guard xBn >= 0 && xBn <= pBn - 1 else { + throw Exception.badKey("Invalid x supplied") + } + + // 2.3 + let yTildeP: BigUInteger + switch prefix { + case 02: + yTildeP = zeroBn + case 03: + yTildeP = oneBn + default: + throw Exception.badKey("Bad public key prefix") + } + + // 2.4, 2.4.1 path + let alpha = (xBn.power(3) + (aBn * xBn) + bBn) % pBn + + // Take square root mod p + // https://en.wikipedia.org/wiki/Tonelli%E2%80%93Shanks_algorithm + // For prime p = 3 (mod 4), r = n ^ ((p+1)/n) (mod p) is a root of r^2 = n (mod p), if a square root exists + let beta = alpha.power((pBn + 1)/4, modulus: pBn) + + // check square root exists + guard beta.power(2, modulus: pBn) == alpha else { + throw Exception.badKey("Square root not found") + } + + let yBn: BigUInteger + if beta % 2 == yTildeP { + yBn = beta + } else { + yBn = pBn - beta + } + + let y = Bytes(yBn.serialize()) + + // must fit in 48 bytes to be valid + guard y.count <= 48 else { + throw Exception.badKey("Invalid y byte length") + } + + // prefix pad y bytes to fill 48 bytes + let yPadded = Bytes(repeating: 0, count: 48 - y.count) + y + + // 2.5 + return (x, yPadded) + } + + fileprivate static func decompress(compressedKey: Bytes) throws -> P384.Signing.PublicKey { + let (x, y) = try decompressToCoords(compressedKey: compressedKey) + return try P384.Signing.PublicKey(rawRepresentation: x + y) + } + + fileprivate static func compress(key: P384.Signing.PublicKey) -> Bytes { let x963Representation = key.x963Representation.bytes guard x963Representation[0] == 04 else { @@ -49,6 +137,10 @@ extension Version3.Public { return [prefix] + xBytes } + var compressed: Bytes { + Self.compress(key: key) + } + public var material: Bytes { compressed } @@ -60,16 +152,33 @@ extension Version3.Public { ) } - guard - let key = try? P384.Signing.PublicKey(x963Representation: material) else { - throw Exception.badKey("Public key is invalid") - } + self.key = try Self.decompress(compressedKey: material) + } + init (_ key: P384.Signing.PublicKey) { self.key = key } - init (key: P384.Signing.PublicKey) { - self.key = key + // Imports a P384.Signing.PublicKey. This method will throw if the public key + // contains an invalid curve point. + public init (key: P384.Signing.PublicKey) throws { + // Rather than explicitly checking the co-ordinates here, the stratergy is + // to export the public key raw and compressed, then use the safe compressed + // constructor. If we detect a change between the imported key and the + // original key then we error. + + // store starting bytes + let givenRawBytes = key.rawRepresentation.bytes + + // compress and parse as compressed + try self.init(material: Self.compress(key: key)) + + let parsedRawBytes = self.key.rawRepresentation.bytes + + // assert that parsed compressed bytes match input bytes + guard Util.equals(givenRawBytes, parsedRawBytes) else { + throw Exception.badKey("Public key is invalid") + } } } } diff --git a/Sources/Paseto/Implementations/Version3/Public/V3PublicAsymmetricSecretKey.swift b/Sources/Paseto/Implementations/Version3/Public/V3PublicAsymmetricSecretKey.swift index 22cba2c..3f1883d 100644 --- a/Sources/Paseto/Implementations/Version3/Public/V3PublicAsymmetricSecretKey.swift +++ b/Sources/Paseto/Implementations/Version3/Public/V3PublicAsymmetricSecretKey.swift @@ -46,9 +46,7 @@ extension Version3.Public.AsymmetricSecretKey: Paseto.AsymmetricSecretKey { } public var publicKey: Version3.Public.AsymmetricPublicKey { - return Version3.Public.AsymmetricPublicKey ( - key: key.publicKey - ) + return Version3.Public.AsymmetricPublicKey(key.publicKey) } } diff --git a/Tests/PasetoTests/KeyTest.swift b/Tests/PasetoTests/KeyTest.swift new file mode 100644 index 0000000..1e54332 --- /dev/null +++ b/Tests/PasetoTests/KeyTest.swift @@ -0,0 +1,45 @@ +import XCTest +@testable import Paseto +import CryptoKit +import Sodium + +class KeyTest: XCTestCase { + @available(macOS 11, iOS 14, watchOS 7, tvOS 14, macCatalyst 14, *) + func testInvalidKeyImport() { + let material = sodium.utils.hex2bin( "04fbcb7c69ee1c60579be7a334134878d9c5c5bf35d552dab63c0140397ed14cef637d7720925c44699ea30e72874c72fbfbcb7c69ee1c60579be7a334134878d9c5c5bf35d552dab63c0140397ed14cef637d7720925c44699ea30e72874c72fb")! + + // import invalid key + let pubKey = try! P384.Signing.PublicKey(x963Representation: material) + + // should detect invalid key + XCTAssertThrowsError(try Paseto.Version3.AsymmetricPublicKey(key: pubKey)) + } + +#if compiler(>=5.7) + @available(macOS 13, *) + func testGeneratedKeyImport() { + for _ in 1...100 { + let privKey = P384.Signing.PrivateKey(compactRepresentable: false) + + let pubKey = privKey.publicKey + + let pasetoPubKey = Paseto.Version3.AsymmetricPublicKey(bytes: pubKey.compressedRepresentation)! + + XCTAssertEqual(pubKey.rawRepresentation.bytes, pasetoPubKey.key.rawRepresentation.bytes) + } + } + + @available(macOS 13, *) + func testRandomKeyImport() { + for _ in 1...100 { + let bytes = [Util.random(length: 1)[0] % 2 == 0 ? 02 : 03] + Util.random(length: 48) + + let pasetoPubKey = Paseto.Version3.AsymmetricPublicKey(bytes: bytes) + let pubKey = try? P384.Signing.PublicKey(compressedRepresentation: bytes) + + XCTAssertEqual(pubKey?.rawRepresentation.bytes, pasetoPubKey?.key.rawRepresentation.bytes) + } + } +#endif +} +