Skip to content

Commit

Permalink
Merge pull request #18 from frederikbosch/patch-2
Browse files Browse the repository at this point in the history
Allow public init from P384 PublicKey
  • Loading branch information
aidantwoods authored Mar 3, 2023
2 parents d650a6e + fa0c69c commit 811e258
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 14 deletions.
14 changes: 10 additions & 4 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
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

vectors-not-stale:
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -49,6 +137,10 @@ extension Version3.Public {
return [prefix] + xBytes
}

var compressed: Bytes {
Self.compress(key: key)
}

public var material: Bytes {
compressed
}
Expand All @@ -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")
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
45 changes: 45 additions & 0 deletions Tests/PasetoTests/KeyTest.swift
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 811e258

Please sign in to comment.