From b231bada4c17aa129961f7e8f92f5f01bb8fad0f Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Fri, 24 Mar 2023 14:54:27 +0100 Subject: [PATCH] Major refactor / rewrite (#16) --- ...4402FEFC-8036-409F-AFED-19F9F70B9B2E.plist | 22 + .../xcbaselines/K1Tests.xcbaseline/Info.plist | 33 + .../xcode/xcshareddata/xcschemes/K1.xcscheme | 105 +++ LICENSE.txt | 202 +++++ Package.resolved | 16 - Package.swift | 44 +- README.md | 228 +++++- .../ECDSASignature+Deprecated.swift | 11 - Sources/K1/K1.swift | 6 + Sources/K1/K1/ECDH/KeyAgreement.swift | 119 +++ Sources/K1/K1/ECDSA/ECDSA.swift | 80 ++ ...SASignatureNonRecoverable+Conversion.swift | 27 - .../ECDSA/ECDSASignatureNonRecoverable.swift | 247 +++++- .../K1/ECDSA/ECDSASignatureRecoverable.swift | 376 +++++++-- Sources/K1/K1/ECDSA/RecoveryID.swift | 39 + .../ECDSA/Signature+Format+Conversion.swift | 78 -- Sources/K1/K1/K1/Context.swift | 18 - Sources/K1/K1/K1/Error.swift | 71 -- Sources/K1/K1/K1/Format.swift | 35 - Sources/K1/K1/K1/K1.swift | 27 - .../K1/K1/Keys/PrivateKey/PrivateKey.swift | 181 +++++ .../PrivateKey/PrivateKey+Bridge+To+C.swift | 639 --------------- .../PrivateKey/PrivateKey/PrivateKey.swift | 80 -- .../PrivateKey/PrivateKeyOf+Feature.swift | 62 ++ .../Wrapped/PrivateKey+Wrapped.swift | 120 --- Sources/K1/K1/Keys/PublicKey/PublicKey.swift | 147 ++++ .../PublicKey/PublicKey+Bridge+To+C.swift | 423 ---------- .../Keys/PublicKey/PublicKey/PublicKey.swift | 50 -- .../Keys/PublicKey/PublicKeyOf+Feature.swift | 58 ++ .../PublicKey+Wrapped+Bridge+To+C.swift | 170 ---- .../PublicKey/Wrapped/PublicKey+Wrapped.swift | 52 -- Sources/K1/K1/Schnorr/Schnorr+Signature.swift | 33 + Sources/K1/K1/Schnorr/Schnorr.swift | 147 ++++ .../Extensions/Data+Extensions.swift | 7 - .../K1/Support/FFI/API/ECDH/FFI+ECDH.swift | 98 +++ .../K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift | 102 +++ .../ECDSA+NonRecovery+Wrapped.swift | 34 + .../NonRecovery/FFI+ECDSA+NonRecovery.swift | 174 +++++ .../Recovery/ECDSA+Recovery+Wrapped.swift | 33 + .../ECDSA/Recovery/FFI+ECDSA+Recovery.swift | 122 +++ .../Keys/PrivateKey/PrivateKey+Wrapped.swift | 91 +++ .../API/Keys/PublicKey/FFI+PublicKey.swift | 109 +++ .../Keys/PublicKey/PublicKey+Wrapped.swift | 50 ++ .../Support/FFI/API/Schnorr/FFI+Schnorr.swift | 97 +++ .../FFI/API/Schnorr/Schnorr+Wrapped.swift | 47 ++ .../FFI/Internals/FFI+Call.swift} | 16 +- .../FFI/Internals/FFI+Context.swift} | 34 +- .../K1/Support/FFI/Internals/FFI+Format.swift | 53 ++ .../K1/Support/FFI/Internals/FFI+Raw.swift | 70 ++ Sources/K1/Support/Misc/Curve.swift | 24 + Sources/K1/Support/Misc/K1+Error.swift | 57 ++ Sources/K1/Support/Misc/SharedSecret.swift | 34 + .../ThirdyParty/swift-crypto/ASN1/ASN1.swift | 728 ++++++++++++++++++ .../ASN1/Basic ASN1 Types/ASN1Any.swift | 70 ++ .../ASN1/Basic ASN1 Types/ASN1BitString.swift | 67 ++ .../ASN1/Basic ASN1 Types/ASN1Boolean.swift | 57 ++ .../Basic ASN1 Types/ASN1Identifier.swift | 146 ++++ .../ASN1/Basic ASN1 Types/ASN1Integer.swift | 272 +++++++ .../ASN1/Basic ASN1 Types/ASN1Null.swift | 44 ++ .../Basic ASN1 Types/ASN1OctetString.swift | 60 ++ .../ASN1/Basic ASN1 Types/ASN1Strings.swift | 196 +++++ .../Basic ASN1 Types/ArraySliceBigint.swift | 40 + .../Basic ASN1 Types/GeneralizedTime.swift | 384 +++++++++ .../Basic ASN1 Types/ObjectIdentifier.swift | 222 ++++++ .../swift-crypto/ASN1/ECDSASignature.swift | 59 ++ .../swift-crypto/ASN1/PEMDocument.swift | 118 +++ .../swift-crypto/ASN1/PKCS8PrivateKey.swift | 103 +++ .../swift-crypto/ASN1/SEC1PrivateKey.swift | 108 +++ .../ASN1/SubjectPublicKeyInfo.swift | 125 +++ .../ThirdyParty/swift-crypto/ASN1Error.swift | 43 ++ .../ThirdyParty/swift-crypto/BytesUtil.swift | 22 +- .../ThirdyParty/swift-crypto/RNG_boring.swift | 0 .../swift-crypto/SafeCompare_boring.swift | 2 +- .../swift-crypto/SecureBytes.swift | 2 +- .../swift-crypto/SafeCompare.swift | 24 - Sources/K1/Util/MemoizationBox.swift | 36 - Sources/secp256k1/include/secp256k1.h | 2 +- Sources/secp256k1/src/ecdh_no_hashfp.h | 21 - .../src/{ecdh_no_hashfp.c => ecdh_variants.c} | 8 +- Sources/secp256k1/src/ecdh_variants.h | 39 + Tests/K1Tests/TestCases/ECDH/ECDHTests.swift | 39 +- .../TestCases/ECDH/ECDHWychoproofTests.swift | 8 +- .../ECDH/TwoVariantsOfECDHWithKDFTests.swift | 124 +-- ...SARecoverableSignatureRoundtripTests.swift | 6 +- .../TestCases/ECDSA/ECDSASignatureTests.swift | 34 +- .../ECDSA/ECDSASignatureTrezorTests.swift | 34 +- ...cheproofASNDEREncodedSignaturesTests.swift | 8 +- ...oofIEEEP1364RSEncodedSignaturesTests.swift | 6 +- .../PrivateKey/PrivateKeyEncodingTests.swift | 54 ++ .../PrivateKeyGenerationTests.swift | 4 +- .../PrivateKey/PrivateKeyImportTests.swift | 18 +- .../PublicKey/PublicKeyEncodingTests.swift | 91 +++ .../Keys/PublicKey/PublicKeyImportTests.swift | 31 +- .../Performance/PerformanceTests.swift | 88 +++ .../ECDASignaturePublicKeyRecoveryTests.swift | 86 ++- .../SchnorrSignatureBitcoinCoreTests.swift | 36 +- .../Schnorr/SchnorrSignatureTests.swift | 7 +- ...256_test.json => bip340_schnorr_sign.json} | 0 ...6_test.json => bip340_schnorr_verify.json} | 0 ...n => cyon_ecdh_two_variants_with_kdf.json} | 0 ...st.json => trezor_ecdsa_sign_rfc6979.json} | 0 ...son => warta_cyon_publickey_recovery.json} | 2 + ...est.json => wycheproof_ecdh_ASN1x963.json} | 0 ....json => wycheproof_ecdsa_verify_der.json} | 0 ...son => wycheproof_ecdsa_verify_p1363.json} | 0 Tests/K1Tests/Util/Data+Hex.swift | 16 - Tests/K1Tests/Util/ECSignature.swift | 13 + Tests/K1Tests/Util/PublicKey+DERDecode.swift | 74 -- Tests/K1Tests/Util/Wycheproof.swift | 62 +- Tests/LinuxMain.swift | 1 - scripts/build.sh | 11 + 111 files changed, 6604 insertions(+), 2445 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcbaselines/K1Tests.xcbaseline/4402FEFC-8036-409F-AFED-19F9F70B9B2E.plist create mode 100644 .swiftpm/xcode/xcshareddata/xcbaselines/K1Tests.xcbaseline/Info.plist create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/K1.xcscheme create mode 100644 LICENSE.txt delete mode 100644 Package.resolved delete mode 100644 Sources/K1/Deprecations/ECDSASignature+Deprecated.swift create mode 100644 Sources/K1/K1.swift create mode 100644 Sources/K1/K1/ECDH/KeyAgreement.swift create mode 100644 Sources/K1/K1/ECDSA/ECDSA.swift delete mode 100644 Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable+Conversion.swift create mode 100644 Sources/K1/K1/ECDSA/RecoveryID.swift delete mode 100644 Sources/K1/K1/ECDSA/Signature+Format+Conversion.swift delete mode 100644 Sources/K1/K1/K1/Context.swift delete mode 100644 Sources/K1/K1/K1/Error.swift delete mode 100644 Sources/K1/K1/K1/Format.swift delete mode 100644 Sources/K1/K1/K1/K1.swift create mode 100644 Sources/K1/K1/Keys/PrivateKey/PrivateKey.swift delete mode 100644 Sources/K1/K1/Keys/PrivateKey/PrivateKey/PrivateKey+Bridge+To+C.swift delete mode 100644 Sources/K1/K1/Keys/PrivateKey/PrivateKey/PrivateKey.swift create mode 100644 Sources/K1/K1/Keys/PrivateKey/PrivateKeyOf+Feature.swift delete mode 100644 Sources/K1/K1/Keys/PrivateKey/Wrapped/PrivateKey+Wrapped.swift create mode 100644 Sources/K1/K1/Keys/PublicKey/PublicKey.swift delete mode 100644 Sources/K1/K1/Keys/PublicKey/PublicKey/PublicKey+Bridge+To+C.swift delete mode 100644 Sources/K1/K1/Keys/PublicKey/PublicKey/PublicKey.swift create mode 100644 Sources/K1/K1/Keys/PublicKey/PublicKeyOf+Feature.swift delete mode 100644 Sources/K1/K1/Keys/PublicKey/Wrapped/PublicKey+Wrapped+Bridge+To+C.swift delete mode 100644 Sources/K1/K1/Keys/PublicKey/Wrapped/PublicKey+Wrapped.swift create mode 100644 Sources/K1/K1/Schnorr/Schnorr+Signature.swift create mode 100644 Sources/K1/K1/Schnorr/Schnorr.swift rename Sources/K1/{ => Support}/Extensions/Data+Extensions.swift (66%) create mode 100644 Sources/K1/Support/FFI/API/ECDH/FFI+ECDH.swift create mode 100644 Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift create mode 100644 Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift create mode 100644 Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift create mode 100644 Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift create mode 100644 Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift create mode 100644 Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift create mode 100644 Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift create mode 100644 Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift create mode 100644 Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift create mode 100644 Sources/K1/Support/FFI/API/Schnorr/Schnorr+Wrapped.swift rename Sources/K1/{Bridge/BridgeToC.swift => Support/FFI/Internals/FFI+Call.swift} (78%) rename Sources/K1/{K1/K1/K1+Bridge+To+C.swift => Support/FFI/Internals/FFI+Context.swift} (55%) create mode 100644 Sources/K1/Support/FFI/Internals/FFI+Format.swift create mode 100644 Sources/K1/Support/FFI/Internals/FFI+Raw.swift create mode 100644 Sources/K1/Support/Misc/Curve.swift create mode 100644 Sources/K1/Support/Misc/K1+Error.swift create mode 100644 Sources/K1/Support/Misc/SharedSecret.swift create mode 100644 Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/ASN1.swift create mode 100644 Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Any.swift create mode 100644 Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1BitString.swift create mode 100644 Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Boolean.swift create mode 100644 Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Identifier.swift create mode 100644 Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Integer.swift create mode 100644 Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Null.swift create mode 100644 Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1OctetString.swift create mode 100644 Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Strings.swift create mode 100644 Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ArraySliceBigint.swift create mode 100644 Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/GeneralizedTime.swift create mode 100644 Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ObjectIdentifier.swift create mode 100644 Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/ECDSASignature.swift create mode 100644 Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/PEMDocument.swift create mode 100644 Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/PKCS8PrivateKey.swift create mode 100644 Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/SEC1PrivateKey.swift create mode 100644 Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/SubjectPublicKeyInfo.swift create mode 100644 Sources/K1/Support/ThirdyParty/swift-crypto/ASN1Error.swift rename Sources/K1/{ => Support}/ThirdyParty/swift-crypto/BytesUtil.swift (96%) rename Sources/K1/{ => Support}/ThirdyParty/swift-crypto/RNG_boring.swift (100%) rename Sources/K1/{ => Support}/ThirdyParty/swift-crypto/SafeCompare_boring.swift (92%) rename Sources/K1/{ => Support}/ThirdyParty/swift-crypto/SecureBytes.swift (99%) delete mode 100644 Sources/K1/ThirdyParty/swift-crypto/SafeCompare.swift delete mode 100644 Sources/K1/Util/MemoizationBox.swift delete mode 100644 Sources/secp256k1/src/ecdh_no_hashfp.h rename Sources/secp256k1/src/{ecdh_no_hashfp.c => ecdh_variants.c} (51%) create mode 100644 Sources/secp256k1/src/ecdh_variants.h create mode 100644 Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyEncodingTests.swift create mode 100644 Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyEncodingTests.swift create mode 100644 Tests/K1Tests/TestCases/Performance/PerformanceTests.swift rename Tests/K1Tests/TestVectors/{schnorr_secp256k1_sign_sha256_test.json => bip340_schnorr_sign.json} (100%) rename Tests/K1Tests/TestVectors/{schnorr_secp256k1_verify_sha256_test.json => bip340_schnorr_verify.json} (100%) rename Tests/K1Tests/TestVectors/{ecdh_secp256k1_two_variants_with_kdf_test.json => cyon_ecdh_two_variants_with_kdf.json} (100%) rename Tests/K1Tests/TestVectors/{ecdsa_secp256k1_sha256_rfc6979_trezor_test.json => trezor_ecdsa_sign_rfc6979.json} (100%) rename Tests/K1Tests/TestVectors/{publickey_recovery.json => warta_cyon_publickey_recovery.json} (99%) rename Tests/K1Tests/TestVectors/{ecdh_secp256k1_test.json => wycheproof_ecdh_ASN1x963.json} (100%) rename Tests/K1Tests/TestVectors/{ecdsa_secp256k1_sha256_der_test.json => wycheproof_ecdsa_verify_der.json} (100%) rename Tests/K1Tests/TestVectors/{ecdsa_secp256k1_sha256_p1363_RS_test.json => wycheproof_ecdsa_verify_p1363.json} (100%) delete mode 100644 Tests/K1Tests/Util/Data+Hex.swift create mode 100644 Tests/K1Tests/Util/ECSignature.swift delete mode 100644 Tests/K1Tests/Util/PublicKey+DERDecode.swift delete mode 100644 Tests/LinuxMain.swift create mode 100755 scripts/build.sh diff --git a/.swiftpm/xcode/xcshareddata/xcbaselines/K1Tests.xcbaseline/4402FEFC-8036-409F-AFED-19F9F70B9B2E.plist b/.swiftpm/xcode/xcshareddata/xcbaselines/K1Tests.xcbaseline/4402FEFC-8036-409F-AFED-19F9F70B9B2E.plist new file mode 100644 index 0000000..f01771c --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcbaselines/K1Tests.xcbaseline/4402FEFC-8036-409F-AFED-19F9F70B9B2E.plist @@ -0,0 +1,22 @@ + + + + + classNames + + PerformanceTests + + testPerformance() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.029900 + baselineIntegrationDisplayName + Local Baseline + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcbaselines/K1Tests.xcbaseline/Info.plist b/.swiftpm/xcode/xcshareddata/xcbaselines/K1Tests.xcbaseline/Info.plist new file mode 100644 index 0000000..888790c --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcbaselines/K1Tests.xcbaseline/Info.plist @@ -0,0 +1,33 @@ + + + + + runDestinationsByUUID + + 4402FEFC-8036-409F-AFED-19F9F70B9B2E + + localComputer + + busSpeedInMHz + 0 + cpuCount + 1 + cpuKind + Apple M1 Pro + cpuSpeedInMHz + 0 + logicalCPUCoresPerPackage + 10 + modelCode + MacBookPro18,3 + physicalCPUCoresPerPackage + 10 + platformIdentifier + com.apple.platform.macosx + + targetArchitecture + arm64 + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/K1.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/K1.xcscheme new file mode 100644 index 0000000..d7b025a --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/K1.xcscheme @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index ecb0362..0000000 --- a/Package.resolved +++ /dev/null @@ -1,16 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "ASN1Decoder", - "repositoryURL": "https://github.com/filom/ASN1Decoder", - "state": { - "branch": null, - "revision": "65953a42a0f039f53c73e48fd88c02809f7db607", - "version": "1.8.0" - } - } - ] - }, - "version": 1 -} diff --git a/Package.swift b/Package.swift index 5642959..0adfee2 100644 --- a/Package.swift +++ b/Package.swift @@ -3,6 +3,21 @@ import PackageDescription +let cSettings: [CSetting] = [ + // Basic config values that are universal and require no dependencies. + // https://github.com/bitcoin-core/secp256k1/blob/master/src/basic-config.h#L12-L13 + .define("ECMULT_WINDOW_SIZE", to: "15"), + .define("ECMULT_GEN_PREC_BITS", to: "4"), + + // Enable modules in secp256k1. + // See bottom of: Sources/secp256k1/libsecp256k1/src/secp256k1.c + // For list + .define("ENABLE_MODULE_ECDH"), + .define("ENABLE_MODULE_RECOVERY"), + .define("ENABLE_MODULE_SCHNORRSIG"), + .define("ENABLE_MODULE_EXTRAKEYS"), +] + let package = Package( name: "K1", platforms: [ @@ -17,10 +32,7 @@ let package = Package( ] ), ], - dependencies: [ - // Only used by tests - .package(url: "https://github.com/filom/ASN1Decoder", from: "1.8.0") - ], + dependencies: [], targets: [ // Target `libsecp256k1` https://github.com/bitcoin-core/secp256k1 @@ -50,37 +62,21 @@ let package = Package( "libsecp256k1/README.md", "libsecp256k1/SECURITY.md" ], - cSettings: [ - // Basic config values that are universal and require no dependencies. - // https://github.com/bitcoin-core/secp256k1/blob/master/src/basic-config.h#L12-L13 - .define("ECMULT_WINDOW_SIZE", to: "15"), - .define("ECMULT_GEN_PREC_BITS", to: "4"), - - // Enable modules in secp256k1. - // See bottom of: Sources/secp256k1/libsecp256k1/src/secp256k1.c - // For list - .define("ENABLE_MODULE_ECDH"), - .define("ENABLE_MODULE_RECOVERY"), - .define("ENABLE_MODULE_SCHNORRSIG"), - .define("ENABLE_MODULE_EXTRAKEYS"), - ] + cSettings: cSettings ), - .target( name: "K1", dependencies: [ - // ECDSA, Schnorr, ECDH etc. "secp256k1", + ], + swiftSettings: [ + .define("CRYPTO_IN_SWIFTPM_FORCE_BUILD_API"), ] ), - .testTarget( name: "K1Tests", dependencies: [ "K1", - - // DER decoding of public keys from test vectors - "ASN1Decoder" ], resources: [ .process("TestVectors/"), diff --git a/README.md b/README.md index fd047e5..a6f59f0 100644 --- a/README.md +++ b/README.md @@ -3,38 +3,184 @@ _K1_ is Swift wrapper around [libsecp256k1 (bitcoin-core/secp256k1)][lib], offering ECDSA, Schnorr ([BIP340][bip340]) and ECDH features. -# Features +# API +The API of K1 maps almost 1:1 with Apple's [CryptoKit][ck], vendoring a set of keypairs, one per feature. E.g. in CryptoKit you have `Curve25519.KeyAgreement.PrivateKey` and `Curve25519.KeyAgreement.PublicKey` which are seperate for `Curve25519.Signing.PrivateKey` and `Curve25519.Signing.PublicKey`. -## ECDSA Signatures +Just like that K1 vendors these key pairs: +- `K1.KeyAgreement.PrivateKey` / `K1.KeyAgreement.PublicKey` for key agreement (ECDH) +- `K1.Schnorr.PrivateKey` / `K1.Schnorr.PublicKey` for sign / verify methods using Schnorr signature scheme +- `K1.ECDSA.Recoverable.PrivateKey` / `K1.ECDSA.Recoverable.PublicKey` for sign / verify methods using ECDSA (producing/validating signature where public key is recoverable) +- `K1.ECDSA.NonRecoverable.PrivateKey` / `K1.ECDSA.NonRecoverable.PublicKey` for sign / verify methods using ECDSA (producing/validating signature where public key is **not** recoverable) + +Just like you can convert between e.g. `Curve25519.KeyAgreement.PrivateKey` and `Curve25519.Signing.PrivateKey` back and forth using any of the initializers and serializer, you can convert between all PrivateKeys and all PublicKeys of all features in K1. + +All keys conform to `K1KeyExportable` protocol below to serialize the Private/PubliKey: + +```swift +public protocol K1KeyExportable { + var rawRepresentation: Data { get } + var x963Representation: Data { get } + var derRepresentation: Data { get } + var pemRepresentation: String { get } +} +``` + +All keys conform to `K1KeyImportable` protocol below to deserialize to Private/PubliKey: ```swift -let alice = try K1.PrivateKey.generateNew() -let message = "Send Bob 3 BTC".data(using: .utf8)! -let signature = try alice.ecdsaSign(unhashed: message) -let isSignatureValid = try alice.publicKey.isValidECDSASignature(signature, unhashed: message) -assert(isSignatureValid, "Signature should be valid.") +public protocol K1KeyImportable { + init(rawRepresentation: some ContiguousBytes) throws + init(x963Representation: some ContiguousBytes) throws + init(derRepresentation: some RandomAccessCollection) throws + init(pemRepresentation: String) throws +} ``` +Furthermore, all PrivateKey's conform to this protocol + +```swift +protocol K1PrivateKeyProtocol: K1KeyPortable { + associatedtype PublicKey: K1PublicKeyProtocol + var publicKey: PublicKey { get } + init() +} +``` + +Furthermore, all PublicKey's conform to this protocol + +```swift +public protocol K1PublicKeyProtocol: K1KeyPortable { + init(compressedRepresentation: some ContiguousBytes) throws + var compressedRepresentation: Data { get } +} + +``` + + +# ECDSA (Elliptic Curve Digital Signature Algorithm) + +There exists two set of ECDSA key pairs: +- A key pair for signatures from which you can recover the public key, specifically: `K1.ECDSA.Recoverable.PrivateKey` and `K1.ECDSA.Recoverable.PublicKey` +- A key pair for signatures from which you can **not** recover the public key, specifically: `K1.ECDSA.NonRecoverable.PrivateKey` and `K1.ECDSA.NonRecoverable.PublicKey` + +For each private key there exists two different `signature:for:options` (one taking hashed data and taking `Digest` as argument) methods and one `signature:forUnhashed:options`. + +The `option` is a `K1.ECDSA.SigningOptions` struct, which by default specifies [`RFC6979`][rfc6979] deterministic signing, as per Bitcoin standard, however, you can change to use secure random nonce instead. -## Schnorr Signatures +## NonRecoverable +### Sign ```swift -let alice = try K1.PrivateKey.generateNew() -let message = "Send Bob 3 BTC".data(using: .utf8)! -let signature = try alice.schnorrSign(unhashed: message) -let isSignatureValid = try alice.publicKey.isValidSchnorrSignature(signature, unhashed: message) -assert(isSignatureValid, "Signature should be valid.") +let alice = K1.ECDA.NonRecovarable.PrivateKey() ``` -### Schnorr Scheme +#### Hashed (Data) + +```swift +let hashedMessage: Data = // from somewhere +let signature = try alice.signature(for: hashedMessage) +``` + +#### Digest + +```swift +let message: Data = // from somewhere +let digest = SHA256.hash(data: message) +let signature = try alice.signature(for: digest) +``` + +#### Hash and Sign + +The `forUnhashed` will `SHA256` hash the message and then sign it. + +```swift +let message: Data = // from somewhere +let signature = try alice.signature(forUnhashed: message) +``` + +### Validate + +#### Hashed (Data) + +```swift +let hashedMessage: Data = // from somewhere +let publicKey: K1.ECDSA.NonRecoverable.PublicKey = alice.publcKey +let signature: K1.ECDSA.NonRecoverable.Signature // from above + +assert( + publicKey.isValidSignature(signature, hashed: hashedMessage) +) // PASS +``` + +#### Digest + +```swift +let message: Data = // from somewhere +let digest = SHA256.hash(data: message) +let signature: K1.ECDSA.NonRecoverable.Signature // from above + +assert( + publicKey.isValidSignature(signature, digest: digest) +) // PASS +``` + +#### Hash and Validate + +```swift +let message: Data = // from somewhere +let signature: K1.ECDSA.NonRecoverable.Signature // from above + +assert( + publicKey.isValidSignature(signature, unhashed: message) +) // PASS +``` + + +## Recoverable + +All signing and validation APIs are identical to the `NonRecoverable` namespace. + +```swift +let alice = K1.ECDA.Recovarable.PrivateKey() +let message: Data = // from somewhere +let digest = SHA256.hash(data: message) +let signature: K1.ECDSA.Recoverable.Signature = try alice.signature(for: digest) +let publicKey: K1.ECDSA.Recoverable.PublicKey = alice.publicKey +assert( + publicKey.isValidSignature(signature, digest: digest) +) // PASS +``` + + +# Schnorr Signature Scheme + +## Sign + +```swift +let alice = K1.Schnorr.PrivateKey() +let signature = try alice.signature(forUnhashed: message) +``` + +There exists other sign variants, `signature:for:options` (hashed data) and `signature:for:options` (`Digest`) if you already have a hashed message. All three variants takes a `K1.Schnorr.SigningOptions` struct where you can pass `auxiliaryRandomData` to be signed. + +## Validate + +```swift +let publicKey: K1.Schnorr.PublicKey = alice.publicKey +assert(publicKey.isValidSignature(signature, unhashed: message)) // PASS +``` + +Or alternatively `isValidSignature:digest` or `isValidSignature:hashed`. + +#### Schnorr Scheme The Schnorr signature implementation is [BIP340][bip340], since we use _libsecp256k1_ which only provides the [BIP340][bip340] Schnorr scheme. It is worth noting that some Schnorr implementations are incompatible with [BIP340][bip340] and thus this library, e.g. [Zilliqa's](https://github.com/Zilliqa/schnorr/blob/master/src/libSchnorr/src/Schnorr.cpp#L86-L242) ([kudelski report](https://docs.zilliqa.com/zilliqa-schnorr-audit-by-kudelski_public-release.pdf), [libsecp256k1 proposal](https://github.com/bitcoin-core/secp256k1/issues/1070), [Twitter thread](https://twitter.com/AmritKummer/status/1489645007699066886?s=20&t=eDgd5221qEPOVyStY0A8SA)). -## ECDH +# ECDH This library vendors three different EC Diffie-Hellman (ECDH) key exchange functions: 1. `ASN1 x9.63` - No hash, return only the `X` coordinate of the point - `sharedSecretFromKeyAgreement -> SharedSecret` @@ -42,18 +188,18 @@ This library vendors three different EC Diffie-Hellman (ECDH) key exchange funct 3. Custom - No hash, return point uncompressed - `ecdhPoint -> Data` ```swift -let alice = try K1.PrivateKey.generateNew() -let bob = try K1.PrivateKey.generateNew() +let alice = try K1.KeyAgreement.PrivateKey() +let bob = try K1.KeyAgreement.PrivateKey() ``` -### `ASN1 x9.63` ECDH +## `ASN1 x9.63` ECDH Returning only the `X` coordinate of the point, following [ANSI X9.63][x963] standards, embedded in a [`CryptoKit.SharedSecret`][ckss], which is useful since you can use `CryptoKit` key derivation functions on this SharedSecret, e.g. [`x963DerivedSymmetricKey`](https://developer.apple.com/documentation/cryptokit/sharedsecret/x963derivedsymmetrickey(using:sharedinfo:outputbytecount:)) or [`hkdfDerivedSymmetricKey`](https://developer.apple.com/documentation/cryptokit/sharedsecret/hkdfderivedsymmetrickey(using:salt:sharedinfo:outputbytecount:)). You can retrieve the `X` coordinate as raw data using `withUnsafeBytes` if you need to. ```swift let ab: CryptoKit.SharedSecret = try alice.sharedSecretFromKeyAgreement(with: bob.publicKey) -let ba: : CryptoKit.SharedSecret = try bob.sharedSecretFromKeyAgreement(with: alice.publicKey) +let ba: CryptoKit.SharedSecret = try bob.sharedSecretFromKeyAgreement(with: alice.publicKey) assert(ab == ba) // pass @@ -62,19 +208,21 @@ ab.withUnsafeBytes { } ``` -### `libsecp256k1` ECDH +## `libsecp256k1` ECDH -Using `libsecp256k1` default behaviour, returning a SHA-256 hash of the **compressed** point. +Using `libsecp256k1` default behaviour, returning a SHA-256 hash of the **compressed** point, embedded in a [`CryptoKit.SharedSecret`][ckss], which is useful since you can use `CryptoKit` key derivation functions. ```swift -let ab: Data = try alice.ecdh(with: bob.publicKey) -let ba: Data = try bob.ecdh(with: alice.publicKey) +let ab: CryptoKit.SharedSecret = try alice.ecdh(with: bob.publicKey) +let ba: CryptoKit.SharedSecret = try bob.ecdh(with: alice.publicKey) assert(ab == ba) // pass -assert(ab.count == 32) // pass +ab.withUnsafeBytes { + assert(Data($0).count == 32) // pass +} ``` -### Custom ECDH +## Custom ECDH Returns an entire uncompresed EC point, without hashing it. Might be useful if you wanna construct your own cryptographic functions, e.g. some custom ECIES. @@ -87,30 +235,36 @@ assert(ab.count == 65) // pass ``` -# Alternatives - -- [GigaBitcoin/secp256k1.swift](https://github.com/GigaBitcoin/secp256k1.swift) (also using `libsecp256k1`, ⚠️ possibly unsafe, ✅ Schnorr support) -- [KevinVitale/WalletKit](https://github.com/KevinVitale/WalletKit/) (also using `libsecp256k1`, ❌ No Schnorr) -- [leif-ibsen/SwiftECC](https://github.com/leif-ibsen/SwiftECC) (Custom ECC impl, ⚠️ possibly unsafe, ❌ No Schnorr) -- [yenom/BitcoinKit](https://github.com/yenom/BitcoinKit) (💀 Discontinued, also using `libsecp256k1`, ❌ No Schnorr) -- [oleganza/CoreBitcoin](https://github.com/oleganza/CoreBitcoin) (OpenSSL as ECC impl, ObjC + Swift, ⚠️ possibly unsafe, ❌ No Schnorr) -- [Sajjon/EllipticCurveKit](https://github.com/Sajjon/EllipticCurveKit) (Custom ECC impl (mine), ☣️ unsafe, ✅ Schnorr support) +# Acknowledgements +`K1` is a Swift wrapper around [libsecp256k1][lib], so this library would not exist without the Bitcoin Core developers. Massive thank you for a wonder ful library! I've included it as a submodule, without any changes to the code, i.e. with copyright headers in files intact. -## Non-Swift but SPM support -[greymass/secp256k1](https://github.com/greymass/secp256k1) (Fork of `libsecp256k1`) +`K1` uses some code from [`swift-crypto`][swc], which has been copied over with relevant copyright header. Since [`swift-crypto`][swc] is licensed under [Apache](https://github.com/apple/swift-crypto/blob/main/LICENSE.txt), so is this library. # Development Stand in root and run ```sh -git submodule init -git submodule update +./scripts/build.sh ``` To clone the dependency [libsecp256k1][lib], using commit [427bc3cdcfbc74778070494daab1ae5108c71368](https://github.com/bitcoin-core/secp256k1/commit/427bc3cdcfbc74778070494daab1ae5108c71368) (semver 0.3.0) +# Alternatives + +- [GigaBitcoin/secp256k1.swift](https://github.com/GigaBitcoin/secp256k1.swift) (also using `libsecp256k1`, ⚠️ possibly unsafe, ✅ Schnorr support) +- [KevinVitale/WalletKit](https://github.com/KevinVitale/WalletKit/) (also using `libsecp256k1`, ❌ No Schnorr) +- [leif-ibsen/SwiftECC](https://github.com/leif-ibsen/SwiftECC) (Custom ECC impl, ⚠️ possibly unsafe, ❌ No Schnorr) +- [yenom/BitcoinKit](https://github.com/yenom/BitcoinKit) (💀 Discontinued, also using `libsecp256k1`, ❌ No Schnorr) +- [oleganza/CoreBitcoin](https://github.com/oleganza/CoreBitcoin) (OpenSSL as ECC impl, ObjC + Swift, ⚠️ possibly unsafe, ❌ No Schnorr) +- [Sajjon/EllipticCurveKit](https://github.com/Sajjon/EllipticCurveKit) (Custom ECC impl (mine), ☣️ unsafe, ✅ Schnorr support) + + +[ck]: https://developer.apple.com/documentation/cryptokit [BIP340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki [lib]: https://github.com/bitcoin-core/secp256k1 [x963]: https://webstore.ansi.org/standards/ascx9/ansix9632011r2017 [ckss]: https://developer.apple.com/documentation/cryptokit/sharedsecret +[swc]: https://github.com/apple/swift-crypto +[rfc6979]: https://www.rfc-editor.org/rfc/rfc6979 +[mall]: https://en.bitcoin.it/wiki/Transaction_malleability diff --git a/Sources/K1/Deprecations/ECDSASignature+Deprecated.swift b/Sources/K1/Deprecations/ECDSASignature+Deprecated.swift deleted file mode 100644 index d9160fc..0000000 --- a/Sources/K1/Deprecations/ECDSASignature+Deprecated.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-10. -// - -import Foundation - -@available(*, deprecated, message: "'ECDSASignature' is a deprecated typealias for 'ECDSASignatureNonRecoverable', use 'ECDSASignatureNonRecoverable' or ECDSASignatureRecovarable' instead.") -public typealias ECDSASignature = ECDSASignatureNonRecoverable diff --git a/Sources/K1/K1.swift b/Sources/K1/K1.swift new file mode 100644 index 0000000..25aa707 --- /dev/null +++ b/Sources/K1/K1.swift @@ -0,0 +1,6 @@ +import Foundation + +/// The Elliptic Curve `secp256k1` +public enum K1 { + // The namespace for all structs and operations vendored by this library. +} diff --git a/Sources/K1/K1/ECDH/KeyAgreement.swift b/Sources/K1/K1/ECDH/KeyAgreement.swift new file mode 100644 index 0000000..b1b08f1 --- /dev/null +++ b/Sources/K1/K1/ECDH/KeyAgreement.swift @@ -0,0 +1,119 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-24. +// + +import Foundation +import struct CryptoKit.SharedSecret + +public protocol K1Feature { + associatedtype PublicKey: K1PublicKeyProtocol +} + +extension K1 { + + /// A mechanism used to create a shared secret between two users by performing `secp256k1` elliptic curve Diffie Hellman (ECDH) key exchange. + public enum KeyAgreement: K1Feature { + + /// A `secp256k1` private key used for key agreement. + public typealias PrivateKey = PrivateKeyOf + + /// A `secp256k1` public key used for key agreement. + public typealias PublicKey = PublicKeyOf + } +} + +extension K1.KeyAgreement.PrivateKey { + /// Computes a shared secret with the provided public key from another party, + /// returning only the `X` coordinate of the point, following [ANSI X9.63][ansix963] standards. + /// + /// This is one of three ECDH functions, this library vendors, all three versions + /// uses different serialization of the shared EC Point, specifically: + /// 1. `ASN1 x9.63`: No hash, return only the `X` coordinate of the point <- this function + /// 2. `libsecp256k1`: SHA-256 hash the compressed point + /// 3. Custom: No hash, return point uncompressed + /// + /// This function uses 3. i.e. no hash, and returns only the `X` coordinate of the point. + /// This is following the [ANSI X9.63][ansix963] standard serialization of the shared point. + /// + /// Further more this function is compatible with CryptoKit, since it returns a CryptoKit + /// `SharedSecret` struct, thus offering you to use all of CryptoKit's Key Derivation Functions + /// (`KDF`s), which can be called on the `SharedSecret`. + /// + /// As seen on [StackExchange][cryptostackexchange], this version is compatible with the following + /// libraries: + /// - JS: `elliptic` (v6.4.0 in nodeJS v8.2.1) + /// - JS: `crypto` (builtin) - uses openssl under the hood (in nodeJS v8.2.1) + /// - .NET: `BouncyCastle` (BC v1.8.1.3, .NET v2.1.4) + /// - Python: pyca/cryptography (hazmat) + /// + /// [ansix963]: https://webstore.ansi.org/standards/ascx9/ansix9632011r2017 + /// [cryptostackexchange]: https://crypto.stackexchange.com/a/57727 + public func sharedSecretFromKeyAgreement( + with publicKey: PublicKey + ) throws -> SharedSecret { + let data = try FFI.ECDH.keyExchange( + publicKey: publicKey.impl.wrapped, + privateKey: self.impl.wrapped, + serializeOutputFunction: .ansiX963 + ) + return try SharedSecret(data: data) + } + + + /// Computes a shared secret with the provided public key from another party, + /// using `libsecp256k1` default behaviour, returning a hashed of the compressed point. + /// + /// This is one of three ECDH functions, this library vendors, all three versions + /// uses different serialization of the shared EC Point, specifically: + /// 1. `ASN1 x9.63`: No hash, return only the `X` coordinate of the point + /// 2. `libsecp256k1`: SHA-256 hash the compressed point <- this function + /// 3. Custom: No hash, return point uncompressed + /// + /// This function uses 1. i.e.SHA-256 hash the compressed point. + /// This is using the [default behaviour of `libsecp256k1`][libsecp256k1], which does not adhere to any + /// other standard. + /// + /// As seen on [StackExchange][cryptostackexchange], this version is compatible with all + /// libraries which wraps `libsecp256k1`, e.g.: + /// - Python wrapper: secp256k1 (v0.13.2, for python 3.6.4) + /// - JS wrapper: secp256k1 (v3.5.0, for nodeJS v8.2.1) + /// + /// [libsecp256k1]: https://github.com/bitcoin-core/secp256k1/blob/master/src/modules/ecdh/main_impl.h#L27 + /// [cryptostackexchange]: https://crypto.stackexchange.com/a/57727 + /// + public func ecdh( + with publicKey: PublicKey, + arbitraryData: Data? = nil + ) throws -> SharedSecret { + let data = try FFI.ECDH.keyExchange( + publicKey: publicKey.impl.wrapped, + privateKey: self.impl.wrapped, + serializeOutputFunction: .libsecp256kDefault(arbitraryData: arbitraryData) + ) + return try SharedSecret(data: data) + } + + /// Computes a shared secret with the provided public key from another party, + /// returning an uncompressed public point, unhashed. + /// + /// This is one of three ECDH functions, this library vendors, all three versions + /// uses different serialization of the shared EC Point, specifically: + /// 1. `ASN1 x9.63`: No hash, return only the `X` coordinate of the point + /// 2. `libsecp256k1`: SHA-256 hash the compressed point + /// 3. Custom: No hash, return point uncompressed <- this function + /// + /// This function uses 2. i.e. no hash, return point uncompressed + /// **This is not following any standard at all**, but might be useful if you want to write your + /// cryptographic functions, e.g. some ECIES scheme. + /// + public func ecdhPoint(with publicKey: PublicKey) throws -> Data { + try FFI.ECDH.keyExchange( + publicKey: publicKey.impl.wrapped, + privateKey: self.impl.wrapped, + serializeOutputFunction: .noHashWholePoint + ) + } +} diff --git a/Sources/K1/K1/ECDSA/ECDSA.swift b/Sources/K1/K1/ECDSA/ECDSA.swift new file mode 100644 index 0000000..569be98 --- /dev/null +++ b/Sources/K1/K1/ECDSA/ECDSA.swift @@ -0,0 +1,80 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-24. +// + +import Foundation + + +extension K1 { + + /// A mechanism used to create or verify a cryptographic signature using the `secp256k1` elliptic curve digital signature algorithm (ECDSA). + public enum ECDSA {} +} + +extension K1.ECDSA { + public struct ValidationOptions { + + public let malleabilityStrictness: MalleabilityStrictness + + public init(malleabilityStrictness: MalleabilityStrictness) { + self.malleabilityStrictness = malleabilityStrictness + } + + } +} + +extension K1.ECDSA.ValidationOptions { + + public static let `default`: Self = .init( + malleabilityStrictness: .rejected + ) + + /// Whether or not to consider malleable signatures valid. + /// + /// [more]: https://github.com/bitcoin-core/secp256k1/blob/2e5e4b67dfb67950563c5f0ab2a62e25eb1f35c5/include/secp256k1.h#L510-L550 + public enum MalleabilityStrictness { + /// Considers all malleable signatures **invalid**. + case rejected + + /// Accepts malleable signatures valid. + case accepted + } +} + +// MARK: SigningOptions +extension K1.ECDSA { + public struct SigningOptions: Sendable, Hashable { + public let nonceFunction: NonceFunction + + public init(nonceFunction: NonceFunction) { + self.nonceFunction = nonceFunction + } + } +} + +extension K1.ECDSA.SigningOptions { + + public static let `default`: Self = .init(nonceFunction: .deterministic()) + + public enum NonceFunction: Sendable, Hashable { + case random + + /// RFC6979 + case deterministic(arbitraryData: RFC6979ArbitraryData? = nil) + } +} + +extension K1.ECDSA.SigningOptions.NonceFunction { + public struct RFC6979ArbitraryData: Sendable, Hashable { + public let arbitraryData: [UInt8] + public init(arbitraryData: [UInt8]) throws { + guard arbitraryData.count == Curve.Field.byteCount else { + throw K1.Error.incorrectByteCountOfArbitraryDataForNonceFunction + } + self.arbitraryData = arbitraryData + } + } +} diff --git a/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable+Conversion.swift b/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable+Conversion.swift deleted file mode 100644 index e8e1945..0000000 --- a/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable+Conversion.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-27. -// - -// Bridge to C -import secp256k1 -import Foundation - -extension ECDSASignatureNonRecoverable { - /// Initializes ECDSASignatureNonRecoverable from the DER representation. - public static func `import`(fromDER derRepresentation: D) throws -> Self { - let signatureData = try Bridge.importECDSASignature(fromDER: derRepresentation) - return try Self(rawRepresentation: signatureData) - } - - public func compactRepresentation() throws -> Data { - try Bridge.compactRepresentationOfSignature(rawRepresentation: rawRepresentation) - } - - public func derRepresentation() throws -> Data { - try Bridge.derRepresentationOfSignature(rawRepresentation: rawRepresentation) - } -} - diff --git a/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift b/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift index 6c1c824..5b756f2 100644 --- a/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift +++ b/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift @@ -6,44 +6,235 @@ // import Foundation -import CryptoKit -import secp256k1 +import protocol CryptoKit.Digest +import struct CryptoKit.SHA256 -public struct ECDSASignatureNonRecoverable: Sendable, Hashable, ECSignature { +extension K1.ECDSA { - public let rawRepresentation: Data - - public init(rawRepresentation: D) throws { - guard - rawRepresentation.count == Self.byteCount - else { - throw K1.Error.incorrectByteCountOfRawSignature - } + /// A mechanism used to create or verify a cryptographic signature using the `secp256k1` elliptic curve digital signature algorithm (ECDSA), signatures that do not offer recovery of the public key. + public enum NonRecoverable: K1Feature { + + /// A `secp256k1` private key used to create cryptographic signatures, + /// more specifically ECDSA signatures, that do not offer recovery of the public key. + public typealias PrivateKey = PrivateKeyOf + + /// A `secp256k1` public key used to verify cryptographic signatures, + /// more specifically ECDSA signatures, that do not offer recovery of the public key. + public typealias PublicKey = PublicKeyOf + } +} + +extension K1.ECDSA.NonRecoverable { + public struct Signature: Sendable, Hashable, ContiguousBytes { - self.rawRepresentation = Data(rawRepresentation) + typealias Wrapped = FFI.ECDSA.NonRecovery.Wrapped + internal let wrapped: Wrapped + + init(wrapped: Wrapped) { + self.wrapped = wrapped + } + } +} + +// MARK: Sign +extension K1.ECDSA.NonRecoverable.PrivateKey { + + /// Generates an Elliptic Curve Digital Signature Algorithm (ECDSA) signature of _hashed_ data you provide over the `secp256k1` elliptic curve. + /// - Parameters: + /// - hashed: The _hashed_ data to sign. + /// - options: Whether or not to consider malleable signatures valid. + /// - Returns: The signature corresponding to the data. The signing algorithm uses deterministic or random nonces, dependent on `options`, thus either deterministically producing the same signature or the same data and key, or different on every call. + public func signature( + for hashed: some DataProtocol, + options: K1.ECDSA.SigningOptions = .default + ) throws -> K1.ECDSA.NonRecoverable.Signature { + try K1.ECDSA.NonRecoverable.Signature( + wrapped: FFI.ECDSA.NonRecovery.sign( + hashedMessage: [UInt8](hashed), + privateKey: impl.wrapped, + options: options + ) + ) } - public init(compactRepresentation: D) throws { - var signature = secp256k1_ecdsa_signature() - let compactBytes = [UInt8](compactRepresentation) - try Bridge.call(ifFailThrow: .failedToParseSignatureFromCompactRepresentation) { context in - secp256k1_ecdsa_signature_parse_compact( - context, - &signature, - compactBytes + /// Generates an Elliptic Curve Digital Signature Algorithm (ECDSA) signature of the digest you provide over the `secp256k1` elliptic curve. + /// - Parameters: + /// - digest: The digest of the data to sign. + /// - options: Whether or not to consider malleable signatures valid. + /// - Returns: The signature corresponding to the data. The signing algorithm uses deterministic or random nonces, dependent on `options`, thus either deterministically producing the same signature or the same data and key, or different on every call. + public func signature( + for digest: some Digest, + options: K1.ECDSA.SigningOptions = .default + ) throws -> K1.ECDSA.NonRecoverable.Signature { + try signature( + for: Data(digest), + options: options + ) + } + + /// Generates an elliptic curve digital signature algorithm (ECDSA) signature of the given data over the `secp256k1` elliptic curve, using SHA-256 as a hash function. + /// - Parameters: + /// - unhashed: The data hash and then to sign. + /// - options: Whether or not to consider malleable signatures valid. + /// - Returns: The signature corresponding to the data. The signing algorithm uses deterministic or random nonces, dependent on `options`, thus either deterministically producing the same signature or the same data and key, or different on every call. + public func signature( + forUnhashed unhashed: some DataProtocol, + options: K1.ECDSA.SigningOptions = .default + ) throws -> K1.ECDSA.NonRecoverable.Signature { + try signature( + for: SHA256.hash(data: unhashed), + options: options + ) + } +} + + +// MARK: Validate +extension K1.ECDSA.NonRecoverable.PublicKey { + /// Verifies an elliptic curve digital signature algorithm (ECDSA) signature on some _hash_ over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The (non-recoverable) signature to check against the _hashed_ data. + /// - hashed: The _hashed_ data covered by the signature. + /// - options: Whether or not to consider malleable signatures valid. + /// - Returns: A Boolean value that’s true if the signature is valid for the given _hashed_ data. + public func isValidSignature( + _ signature: K1.ECDSA.NonRecoverable.Signature, + hashed: some DataProtocol, + options: K1.ECDSA.ValidationOptions = .default + ) -> Bool { + do { + return try FFI.ECDSA.NonRecovery.isValid( + ecdsaSignature: signature.wrapped, + publicKey: self.impl.wrapped, + message: [UInt8](hashed), + options: options ) + } catch { + return false } + } + + /// Verifies an elliptic curve digital signature algorithm (ECDSA) signature on a digest over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The (non-recoverable) signature to check against the given digest. + /// - digest: The digest covered by the signature. + /// - options: Whether or not to consider malleable signatures valid. + /// - Returns: A Boolean value that’s true if the signature is valid for the given digest. + public func isValidSignature( + _ signature: K1.ECDSA.NonRecoverable.Signature, + digest: some Digest, + options: K1.ECDSA.ValidationOptions = .default + ) -> Bool { + isValidSignature( + signature, + hashed: Data(digest), + options: options + ) + } - try self.init(rawRepresentation: Data( - bytes: &signature.data, - count: MemoryLayout.size(ofValue: signature.data) - )) + /// Verifies an elliptic curve digital signature algorithm (ECDSA) signature on a block of data over the `secp256k1` elliptic curve. + /// + /// The function computes an SHA-256 hash from the data before verifying the signature. If you separately hash the data to be signed, use `isValidSignature(_:digest:input)` with the created digest. Or if you have access to a digest just as `some DataProtocol`, use + /// `isValidSignature(_:hashed:input)`. + /// + /// - Parameters: + /// - signature: The (non-recoverable) signature to check against the given digest. + /// - unhashed: The block of data covered by the signature. + /// - options: Whether or not to consider malleable signatures valid. + /// - Returns: A Boolean value that’s true if the signature is valid for the given block of data. + public func isValidSignature( + _ signature: K1.ECDSA.NonRecoverable.Signature, + unhashed: some DataProtocol, + options: K1.ECDSA.ValidationOptions = .default + ) -> Bool { + isValidSignature( + signature, + digest: SHA256.hash(data: unhashed), + options: options + ) } } -extension ECDSASignatureNonRecoverable { - internal static let byteCount = 2 * K1.Curve.Field.byteCount - public typealias Scheme = ECDSA - public static let scheme: SigningScheme = .ecdsa + +// MARK: Inits +extension K1.ECDSA.NonRecoverable.Signature { + + public init(compactRepresentation: some DataProtocol) throws { + try self.init( + wrapped: FFI.ECDSA.NonRecovery.from(compactBytes: [UInt8](compactRepresentation)) + ) + } + + public init(derRepresentation: some DataProtocol) throws { + try self.init( + wrapped: FFI.ECDSA.NonRecovery.from(derRepresentation: [UInt8](derRepresentation)) + ) + } } +// MARK: ContiguousBytes +extension K1.ECDSA.NonRecoverable.Signature { + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + try wrapped.withUnsafeBytes(body) + } +} + +// MARK: Serialize +extension K1.ECDSA.NonRecoverable.Signature { + + internal var rawRepresentation: Data { + Data(wrapped.bytes) + } + + public func compactRepresentation() throws -> Data { + try FFI.ECDSA.NonRecovery.compact(wrapped) + } + + public func derRepresentation() throws -> Data { + try FFI.ECDSA.NonRecovery.der(wrapped) + } +} + + +// MARK: Recover +extension K1.ECDSA.NonRecoverable.Signature { + public func recoverPublicKey( + recoveryID: K1.ECDSA.Recoverable.Signature.RecoveryID, + message: some DataProtocol + ) throws -> K1.ECDSA.NonRecoverable.PublicKey { + let wrapped = try FFI.ECDSA.NonRecovery.recoverPublicKey( + self.wrapped, + recoveryID: recoveryID.recid, + message: [UInt8](message) + ) + let impl = K1.PublicKeyImpl(wrapped: wrapped) + return K1.ECDSA.NonRecoverable.PublicKey(impl: impl) + } +} + +extension K1.ECDSA.NonRecoverable.Signature { + internal static let byteCount = FFI.ECDSA.Recovery.byteCount +} + +// MARK: Equatable +extension K1.ECDSA.NonRecoverable.Signature { + + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.wrapped.withUnsafeBytes { lhsBytes in + rhs.wrapped.withUnsafeBytes { rhsBytes in + safeCompare(lhsBytes, rhsBytes) + } + } + } +} + +// MARK: Hashable +extension K1.ECDSA.NonRecoverable.Signature { + + public func hash(into hasher: inout Hasher) { + wrapped.withUnsafeBytes { + hasher.combine(bytes: $0) + } + } + +} diff --git a/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift b/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift index c120a60..aa1c45c 100644 --- a/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift +++ b/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift @@ -6,101 +6,343 @@ // import Foundation -import CryptoKit -import secp256k1 - -public struct ECDSASignatureRecoverable: Sendable, Hashable, ECSignature { - - public let rawRepresentation: Data - - public init(compactRepresentation: Data, recoveryID: Int32) throws { - guard - compactRepresentation.count == ECDSASignatureNonRecoverable.byteCount - else { - throw K1.Error.incorrectByteCountOfRawSignature - } - var recoverableSignature = secp256k1_ecdsa_recoverable_signature() - let rs = [UInt8](compactRepresentation) - - try Bridge.call(ifFailThrow: .failedToParseRecoverableSignatureFromCompactRepresentation) { context in - secp256k1_ecdsa_recoverable_signature_parse_compact( - context, - &recoverableSignature, - rs, - recoveryID +import protocol CryptoKit.Digest +import struct CryptoKit.SHA256 + +extension K1.ECDSA { + + /// A mechanism used to create or verify a cryptographic signature using the `secp256k1` elliptic curve digital signature algorithm (ECDSA), signatures that do offers recovery of the public key. + public enum Recoverable: K1Feature { + + /// A `secp256k1` private key used to create cryptographic signatures, + /// more specifically ECDSA signatures that offers recovery of the public key. + public typealias PrivateKey = PrivateKeyOf + + /// A `secp256k1` public key used to verify cryptographic signatures. + /// more specifically ECDSA signatures that offers recovery of the public key. + public typealias PublicKey = PublicKeyOf + } +} + +// MARK: Sign +extension K1.ECDSA.Recoverable.PrivateKey { + + /// Generates an Elliptic Curve Digital Signature Algorithm (ECDSA) signature of _hashed_ data you provide over the `secp256k1` elliptic curve. + /// - Parameters: + /// - hashed: The _hashed_ data to sign. + /// - options: Whether or not to consider malleable signatures valid. + /// - Returns: The signature corresponding to the data. The signing algorithm uses deterministic or random nonces, dependent on `options`, thus either deterministically producing the same signature or the same data and key, or different on every call. + public func signature( + for hashed: some DataProtocol, + options: K1.ECDSA.SigningOptions = .default + ) throws -> K1.ECDSA.Recoverable.Signature { + try K1.ECDSA.Recoverable.Signature( + wrapped: FFI.ECDSA.Recovery.sign( + hashedMessage: [UInt8](hashed), + privateKey: impl.wrapped, + options: options + ) + ) + } + + /// Generates an Elliptic Curve Digital Signature Algorithm (ECDSA) signature of the digest you provide over the `secp256k1` elliptic curve. + /// - Parameters: + /// - digest: The digest of the data to sign. + /// - options: Whether or not to consider malleable signatures valid. + /// - Returns: The signature corresponding to the data. The signing algorithm uses deterministic or random nonces, dependent on `options`, thus either deterministically producing the same signature or the same data and key, or different on every call. + public func signature( + for digest: some Digest, + options: K1.ECDSA.SigningOptions = .default + ) throws -> K1.ECDSA.Recoverable.Signature { + try signature( + for: Data(digest), + options: options + ) + } + + /// Generates an elliptic curve digital signature algorithm (ECDSA) signature of the given data over the `secp256k1` elliptic curve, using SHA-256 as a hash function. + /// - Parameters: + /// - unhashed: The data hash and then to sign. + /// - options: Whether or not to consider malleable signatures valid. + /// - Returns: The signature corresponding to the data. The signing algorithm uses deterministic or random nonces, dependent on `options`, thus either deterministically producing the same signature or the same data and key, or different on every call. + public func signature( + forUnhashed unhashed: some DataProtocol, + options: K1.ECDSA.SigningOptions = .default + ) throws -> K1.ECDSA.Recoverable.Signature { + try signature( + for: SHA256.hash(data: unhashed), + options: options + ) + } + +} + +// MARK: Validate +extension K1.ECDSA.Recoverable.PublicKey { + + /// Verifies a recoverable ECDSA signature on some _hash_ over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The recoverable ECDSA signature to check against the given digest. + /// - hashed: The _hashed_ data covered by the signature. + /// - options: ECDSA validation options used during validation + /// - Returns: A Boolean value that’s true if the signature is valid for the given _hashed_ data. + public func isValidSignature( + _ signature: K1.ECDSA.Recoverable.Signature, + hashed: some DataProtocol, + options: K1.ECDSA.ValidationOptions = .default + ) -> Bool { + do { + let publicKeyNonRecoverable = try K1.ECDSA.NonRecoverable.PublicKey(rawRepresentation: self.rawRepresentation) + let signatureNonRecoverable = try signature.nonRecoverable() + + return publicKeyNonRecoverable.isValidSignature( + signatureNonRecoverable, + hashed: hashed, + options: options ) + } catch { + return false } - self.rawRepresentation = Data( - bytes: &recoverableSignature.data, - count: MemoryLayout.size(ofValue: recoverableSignature.data) + } + + /// Verifies a recoverable ECDSA signature on a digest over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The recoverable ECDSA signature to check against the given digest. + /// - digest: The digest covered by the signature. + /// - options: ECDSA validation options used during validation + /// - Returns: A Boolean value that’s true if the signature is valid for the given digest. + public func isValidSignature( + _ signature: K1.ECDSA.Recoverable.Signature, + digest: some Digest, + options: K1.ECDSA.ValidationOptions = .default + ) -> Bool { + isValidSignature( + signature, + hashed: Data(digest), + options: options ) } - public init(rawRepresentation: D) throws { + /// Verifies a recoverable ECDSA signature on a block of data over the `secp256k1` elliptic curve. + /// + /// The function computes an SHA-256 hash from the data before verifying the signature. If you separately hash the data to be signed, use `isValidSignature(_:digest:input)` with the created digest. Or if you have access to a digest just as `some DataProtocol`, use + /// `isValidSignature(_:hashed:input)`. + /// + /// - Parameters: + /// - signature: The recoverable ECDSA signature to check against the given digest. + /// - unhashed: The block of data covered by the signature. + /// - options: ECDSA validation options used during validation + /// - Returns: A Boolean value that’s true if the signature is valid for the given block of data. + public func isValidSignature( + _ signature: K1.ECDSA.Recoverable.Signature, + unhashed: some DataProtocol, + options: K1.ECDSA.ValidationOptions = .default + ) -> Bool { + isValidSignature( + signature, + digest: SHA256.hash(data: unhashed), + options: options + ) + } + +} - guard - rawRepresentation.count == ECDSASignatureNonRecoverable.byteCount + 1 - else { - throw K1.Error.incorrectByteCountOfRawSignature +extension K1.ECDSA.Recoverable { + public struct Signature: Sendable, Hashable, ContiguousBytes { + + typealias Wrapped = FFI.ECDSA.Recovery.Wrapped + private let wrapped: Wrapped + + internal init(wrapped: Wrapped) { + self.wrapped = wrapped } - self.rawRepresentation = Data(rawRepresentation) + } +} + + +// MARK: Init +extension K1.ECDSA.Recoverable.Signature { + + /// Compact aka `IEEE P1363` aka `R||S`. + public init(compact: Compact) throws { + try self.init( + wrapped: FFI.ECDSA.Recovery.deserialize( + compact: [UInt8](compact.compact), + recoveryID: compact.recoveryID.recid + ) + ) } + /// Compact aka `IEEE P1363` aka `R||S`. + public init(compact: Data, recoveryID: RecoveryID) throws { + try self.init(compact: .init(compact: compact, recoveryID: recoveryID)) + } + + public init( + rawRepresentation: some DataProtocol + ) throws { + try self.init( + wrapped: FFI.ECDSA.Recovery.deserialize(rawRepresentation: rawRepresentation) + ) + } +} + +// MARK: ContiguousBytes +extension K1.ECDSA.Recoverable.Signature { + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + try wrapped.withUnsafeBytes(body) + } } -public extension ECDSASignatureRecoverable { +// MARK: Serialize +extension K1.ECDSA.Recoverable.Signature { - typealias Scheme = ECDSA - static let scheme: SigningScheme = .ecdsa + internal var rawRepresentation: Data { + Data(wrapped.bytes) + } - func nonRecoverable() throws -> ECDSASignatureNonRecoverable { - try Bridge.convertToNonRecoverable(ecdsaSignature: self) + /// Compact aka `IEEE P1363` aka `R||S` with `RecoveryID` + public func compact() throws -> Compact { + let (rs, recid) = try FFI.ECDSA.Recovery.serializeCompact( + wrapped + ) + return try .init( + compact: Data(rs), + recoveryID: .init(recid: recid) + ) } - func compact() throws -> (rs: Data, recoveryID: Int) { - var rsBytes = [UInt8](repeating: 0, count: 64) - var recoveryID: Int32 = 0 + public struct Compact: Sendable, Hashable { + + /// Compact aka `IEEE P1363` aka `R||S`. + public let compact: Data - var recoverableBridgedToC = secp256k1_ecdsa_recoverable_signature() - withUnsafeMutableBytes(of: &recoverableBridgedToC.data) { pointer in - pointer.copyBytes( - from: rawRepresentation.prefix(pointer.count) - ) + public let recoveryID: RecoveryID + + /// Compact aka `IEEE P1363` aka `R||S`. + public init( + compact: Data, + recoveryID: RecoveryID + ) throws { + guard compact.count == Self.byteCountRS else { + throw K1.Error.failedToDeserializeCompactRSRecoverableSignatureInvalidByteCount( + got: compact.count, + expected: Self.byteCountRS + ) + } + self.compact = compact + self.recoveryID = recoveryID } - try Bridge.call( - ifFailThrow: .failedSignatureToConvertRecoverableSignatureToCompact) { context in - secp256k1_ecdsa_recoverable_signature_serialize_compact( - context, - &rsBytes, - &recoveryID, - &recoverableBridgedToC + } +} + +extension K1.ECDSA.Recoverable.Signature.Compact { + + public static let byteCountRS = 2 * Curve.Field.byteCount + public static let byteCount = Self.byteCountRS + 1 + + /// Takes either `R || S || V` data or `V || R || S` data, as per specification of `format`. + public init( + rawRepresentation: some DataProtocol, + format: SerializationFormat + ) throws { + guard rawRepresentation.count == Self.byteCount else { + throw K1.Error.failedToDeserializeCompactRecoverableSignatureInvalidByteCount(got: rawRepresentation.count, expected: Self.byteCount + ) + } + switch format { + case .vrs: + try self.init( + compact: Data(rawRepresentation.suffix(Self.byteCountRS)), + recoveryID: .init(byte: rawRepresentation.first!) // force unwrap OK since we have checked length above. + ) + case .rsv: + try self.init( + compact: Data(rawRepresentation.prefix(Self.byteCountRS)), + recoveryID: .init(byte: rawRepresentation.last!) // force unwrap OK since we have checked length above. ) } - return (rs: Data(rsBytes), recoveryID: Int(recoveryID)) } -} - -public extension ECDSASignatureRecoverable { - typealias ValidationMode = ECDSASignatureNonRecoverable.ValidationMode - typealias SigningMode = ECDSASignatureNonRecoverable.SigningMode - - func wasSigned(by signer: K1.PublicKey, for digest: D, mode: ValidationMode) throws -> Bool where D : Digest { - try nonRecoverable().wasSigned(by: signer, for: digest, mode: mode) + public enum SerializationFormat { + + /// `R || S || V` - the format `libsecp256k1` v0.3.0 uses as internal representation + /// This is the default value of this library. + case rsv + + /// We use `R || S || V` as default values since `libsecp256k1` v0.3.0 uses it as its internal representation. + public static let `default`: Self = .rsv + + /// `V || R || S`. + case vrs } - func wasSigned(by signer: K1.PublicKey, hashedMessage: D, mode: ValidationMode) throws -> Bool where D : DataProtocol { - try nonRecoverable().wasSigned(by: signer, hashedMessage: hashedMessage, mode: mode) + private var v: Data { + recoveryID.vData + } + private var rs: Data { + compact } - static func by(signing hashed: D, with privateKey: K1.PrivateKey, mode: SigningMode) throws -> Self where D : DataProtocol { - try privateKey.ecdsaSignRecoverable(hashed: hashed, mode: mode) + func serialize(format: SerializationFormat) -> Data { + switch format { + case .rsv: + return rs + v + case .vrs: + return v + rs + } } - - func wasSigned(by signer: K1.PublicKey, for digest: D) throws -> Bool where D : Digest { - try nonRecoverable().wasSigned(by: signer, for: digest) +} +extension K1.ECDSA.Recoverable.Signature.RecoveryID { + var vData: Data { + Data( + [UInt8(rawValue)] + ) + } +} + +// MARK: Recovery +extension K1.ECDSA.Recoverable.Signature { + public func recoverPublicKey( + message: some DataProtocol + ) throws -> K1.ECDSA.Recoverable.PublicKey { + let wrapped = try FFI.ECDSA.Recovery.recover(wrapped, message: [UInt8](message)) + let impl = K1.PublicKeyImpl(wrapped: wrapped) + return K1.ECDSA.Recoverable.PublicKey( + impl: impl + ) + } +} + + +// MARK: Conversion +extension K1.ECDSA.Recoverable.Signature { + public func nonRecoverable() throws -> K1.ECDSA.NonRecoverable.Signature { + try K1.ECDSA.NonRecoverable.Signature( + wrapped: FFI.ECDSA.Recovery.nonRecoverable(self.wrapped) + ) + } +} + +// MARK: Equatable +extension K1.ECDSA.Recoverable.Signature { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.wrapped.withUnsafeBytes { lhsBytes in + rhs.wrapped.withUnsafeBytes { rhsBytes in + safeCompare(lhsBytes, rhsBytes) + } + } } +} + +// MARK: Hashable +extension K1.ECDSA.Recoverable.Signature { + public func hash(into hasher: inout Hasher) { + wrapped.withUnsafeBytes { + hasher.combine(bytes: $0) + } + } } diff --git a/Sources/K1/K1/ECDSA/RecoveryID.swift b/Sources/K1/K1/ECDSA/RecoveryID.swift new file mode 100644 index 0000000..00ff388 --- /dev/null +++ b/Sources/K1/K1/ECDSA/RecoveryID.swift @@ -0,0 +1,39 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-19. +// + +import Foundation + +// MARK: Recovery +extension K1.ECDSA.Recoverable.Signature { + + public enum RecoveryID: UInt8, Sendable, Hashable, Codable { + case _0 = 0 + case _1 = 1 + case _2 = 2 + case _3 = 3 + + internal var recid: Int32 { + Int32(rawValue) + } + } +} + +extension K1.ECDSA.Recoverable.Signature.RecoveryID { + public init(byte: UInt8) throws { + guard let self_ = Self(rawValue: byte) else { + throw K1.Error.invalidRecoveryID(got: Int(byte)) + } + self = self_ + } + + public init(recid: Int32) throws { + guard recid <= 3 && recid >= 0 else { + throw K1.Error.invalidRecoveryID(got: Int(recid)) + } + try self.init(byte: UInt8(recid)) + } +} diff --git a/Sources/K1/K1/ECDSA/Signature+Format+Conversion.swift b/Sources/K1/K1/ECDSA/Signature+Format+Conversion.swift deleted file mode 100644 index c2151d9..0000000 --- a/Sources/K1/K1/ECDSA/Signature+Format+Conversion.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2023-03-10. -// - -// Bridge to C -import secp256k1 -import Foundation - -extension Bridge { - /// Initializes ECDSASignatureNonRecoverable from the DER representation. - static func importECDSASignature(fromDER derRepresentation: D) throws -> Data { - let derSignatureBytes = Array(derRepresentation) - var signatureBridgedToC = secp256k1_ecdsa_signature() - - try Bridge.call(ifFailThrow: .failedToParseDERSignature) { context in - secp256k1_ecdsa_signature_parse_der( - context, - &signatureBridgedToC, - derSignatureBytes, - derSignatureBytes.count - ) - } - - let signatureData = Data( - bytes: &signatureBridgedToC.data, - count: MemoryLayout.size(ofValue: signatureBridgedToC.data) - ) - - return signatureData - } - - static func compactRepresentationOfSignature(rawRepresentation: Data) throws -> Data { - - let compactSignatureLength = 64 - var signatureBridgedToC = secp256k1_ecdsa_signature() - var compactSignature = [UInt8](repeating: 0, count: compactSignatureLength) - - withUnsafeMutableBytes(of: &signatureBridgedToC.data) { pointer in - pointer.copyBytes(from: rawRepresentation.prefix(pointer.count)) - } - - - try Bridge.call(ifFailThrow: .failedToSerializeCompactSignature) { context in - secp256k1_ecdsa_signature_serialize_compact( - context, - &compactSignature, - &signatureBridgedToC - ) - - } - - return Data(compactSignature) - } - - static func derRepresentationOfSignature(rawRepresentation: Data) throws -> Data { - var signatureBridgedToC = secp256k1_ecdsa_signature() - var derMaxLength = 75 // in fact max is 73, but we can have some margin. - var derSignature = [UInt8](repeating: 0, count: derMaxLength) - - withUnsafeMutableBytes(of: &signatureBridgedToC.data) { pointer in - pointer.copyBytes(from: rawRepresentation.prefix(pointer.count)) - } - - try Bridge.call(ifFailThrow: .failedToSerializeDERSignature) { context in - secp256k1_ecdsa_signature_serialize_der( - context, - &derSignature, - &derMaxLength, - &signatureBridgedToC - ) - } - - return Data(bytes: &derSignature, count: derMaxLength) - } -} diff --git a/Sources/K1/K1/K1/Context.swift b/Sources/K1/K1/K1/Context.swift deleted file mode 100644 index 2d05bcf..0000000 --- a/Sources/K1/K1/K1/Context.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-27. -// - - -internal extension K1 { - - /// Bridging type used for underlying libsecp256k1 methods: - /// - `secp256k1_context_create` - /// - `secp256k1_context_preallocated_size` - /// - `secp256k1_context_preallocated_create` - enum Context: UInt32 { - case none, sign, verify - } -} diff --git a/Sources/K1/K1/K1/Error.swift b/Sources/K1/K1/K1/Error.swift deleted file mode 100644 index 34a8cc7..0000000 --- a/Sources/K1/K1/K1/Error.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-27. -// - -import Foundation - -public extension K1 { - - enum Error: Swift.Error, Equatable { - - /// The private key scalar was either 0 or larger then the order of the - /// curve. - case invalidPrivateKeyMustNotBeZero - case invalidPrivateKeyMustBeSmallerThanOrder - - case incorrectByteCountOfPrivateKey(got: Int, expected: Int) - - case incorrectByteCountOfRawSignature - case incorrectByteCountOfSignatureNotRecoverable - - case failedToCreateContextForSecp256k1 - case failedToUpdateContextRandomization - case failedToComputePublicKeyFromPrivateKey - case incorrectByteCountOfMessageToValidate - case failedToCompressPublicKey - case failedToUncompressPublicKey - case failedToProduceSharedSecret - case incorrectByteCountOfPublicKey(got: Int, acceptableLengths: [Int]) - case failedToParsePublicKeyFromBytes - case failedToParseDERSignature - case failedToSerializeCompactSignature - case failedToSerializeDERSignature - case failedToECDSASignDigest - case recoverPublicKeyDiscrepancyReceivedSignatureContainingRecoveryIDButFunctionSpecifiesANonMatchingOne - case failedToParseRecoverableSignatureFromECDSASignature - case failedToParseRecoverableSignatureFromCompactRepresentation - case failedToParseSignatureFromCompactRepresentation - case failedSignatureToConvertRecoverableSignatureToCompact - case failedToConvertRecoverableSignatureToNonRecoverable - case failedToRecoverPublicKeyFromSignature - case failedToNormalizeECDSASignature - case failedToSchnorrSignMessageInvalidLength - case failedToInitializeKeyPairForSchnorrSigning - case failedToSchnorrSignDigest - case failedToSchnorrSignDigestProvidedRandomnessInvalidLength - case failedToSchnorrSignErrorGettingPubKeyFromKeyPair - case failedToSchnorrVerifyGettingXFromPubKey - case failedToSchnorrSignDigestDidNotPassVerification - case failedToPerformDiffieHellmanKeyExchange - case incorrectByteCountOfAuxilaryDataForSchnorr - case incorrectByteCountOfMessageToECDSASign - case incorrectByteCountOfArbitraryDataForNonceFunction - case failedToSerializePublicKeyIntoBytes - case expectedPublicKeyToBeValidForSignatureAndMessage - case failedToRecognizeSignatureType(onlySupportedSchemesAre: [SigningScheme]) - } - -} - -extension K1.Error { - static func invalidSizeOfPrivateKey(providedByteCount: Int) -> Self { - .incorrectByteCountOfPrivateKey(got: providedByteCount, expected: K1.Curve.Field.byteCount) - } - - static func incorrectByteCountOfPublicKey(providedByteCount: Int) -> Self { - .incorrectByteCountOfPublicKey(got: providedByteCount, acceptableLengths: K1.Format.allCases.map(\.length)) - } -} diff --git a/Sources/K1/K1/K1/Format.swift b/Sources/K1/K1/K1/Format.swift deleted file mode 100644 index 3655c6f..0000000 --- a/Sources/K1/K1/K1/Format.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-27. -// - - -public extension K1 { - - /// Bridging type for: `secp256k1_ec_pubkey_serialize` - enum Format: UInt32, CaseIterable { - case compressed, uncompressed - } -} - -public extension K1.Format { - - var length: Int { - switch self { - case .compressed: return 33 - case .uncompressed: return 65 - } - } - - init(byteCount: Int) throws { - if byteCount == Self.uncompressed.length { - self = .uncompressed - } else if byteCount == Self.compressed.length { - self = .compressed - } else { - throw K1.Error.incorrectByteCountOfPublicKey(providedByteCount: byteCount) - } - } -} diff --git a/Sources/K1/K1/K1/K1.swift b/Sources/K1/K1/K1/K1.swift deleted file mode 100644 index d0bba9d..0000000 --- a/Sources/K1/K1/K1/K1.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation - -// MARK: - `K1` (secp256k1) -// MARK: - - -/// The Elliptic Curve `secp256k1` -public enum K1 {} - -// MARK: - Curve -// MARK: - -internal extension K1 { - - /// Details about the elliptic curve `secp256k1`. - enum Curve {} -} - -// MARK: - FiniteField -// MARK: - -internal extension K1.Curve { - /// The finite field of the secp256k1 curve. - enum Field {} -} - -internal extension K1.Curve.Field { - /// Finite field members are 256 bits large, i.e. 32 bytes. - static let byteCount = 32 -} diff --git a/Sources/K1/K1/Keys/PrivateKey/PrivateKey.swift b/Sources/K1/K1/Keys/PrivateKey/PrivateKey.swift new file mode 100644 index 0000000..da6c070 --- /dev/null +++ b/Sources/K1/K1/Keys/PrivateKey/PrivateKey.swift @@ -0,0 +1,181 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2022-01-27. +// + +import CryptoKit +import Foundation + +public protocol K1KeyExportable { + var rawRepresentation: Data { get } + var x963Representation: Data { get } + var derRepresentation: Data { get } + var pemRepresentation: String { get } +} + +public protocol K1KeyImportable { + init(rawRepresentation: some ContiguousBytes) throws + init(x963Representation: some ContiguousBytes) throws + init(derRepresentation: some RandomAccessCollection) throws + init(pemRepresentation: String) throws +} + +public typealias K1KeyPortable = K1KeyImportable & K1KeyExportable + +public protocol K1PrivateKeyProtocol: K1KeyPortable { + associatedtype PublicKey: K1PublicKeyProtocol + var publicKey: PublicKey { get } + init() +} + +// MARK: - PrivateKey +extension K1 { + + struct PrivateKeyImpl: Sendable, Hashable, K1PrivateKeyProtocol { + + typealias Wrapped = FFI.PrivateKey.Wrapped + internal let wrapped: Wrapped + + let publicKey: PublicKeyImpl + + internal init(wrapped: Wrapped) { + self.wrapped = wrapped + self.publicKey = PublicKey(wrapped: wrapped.publicKey) + } + } +} + +// MARK: Inits +extension K1.PrivateKeyImpl { + + /// Creates a `secp256k1` private key from a Privacy-Enhanced Mail (PEM) representation. + init( + pemRepresentation: String + ) throws { + let pem = try ASN1.PEMDocument(pemString: pemRepresentation) + + switch pem.type { + case "EC \(Self.pemType)": + let parsed = try ASN1.SEC1PrivateKey(asn1Encoded: Array(pem.derBytes)) + self = try .init(rawRepresentation: parsed.privateKey) + case Self.pemType: + let parsed = try ASN1.PKCS8PrivateKey(asn1Encoded: Array(pem.derBytes)) + self = try .init(rawRepresentation: parsed.privateKey.privateKey) + default: + throw K1.Error.invalidPEMDocument + } + } + + init( + rawRepresentation: some ContiguousBytes + ) throws { + try self.init( + wrapped: FFI.PrivateKey.deserialize(rawRepresentation: rawRepresentation) + ) + } + + init( + x963Representation: some ContiguousBytes + ) throws { + let length = x963Representation.withUnsafeBytes { $0.count } + guard length == Self.x963ByteCount else { + throw K1.Error.incorrectByteCountOfX963PrivateKey(got: length, expected: Self.x963ByteCount) + } + + let publicKeyX963 = x963Representation.bytes.prefix(K1.PublicKeyImpl.x963ByteCount) + let publicKeyFromX963 = try K1.PublicKeyImpl.init(x963Representation: publicKeyX963) + let privateKeyRaw = x963Representation.bytes.suffix(Self.rawByteCount) + try self.init(rawRepresentation: privateKeyRaw) + guard self.publicKey == publicKeyFromX963 else { + throw K1.Error.invalidPrivateX963RepresentationPublicKeyDiscrepancy + } + // All good + } + + + /// `DER` + init(derRepresentation: some RandomAccessCollection) throws { + let bytes = Array(derRepresentation) + + // We have to try to parse this twice because we have no informaton about what kind of key this is. + // We try with PKCS#8 first, and then fall back to SEC.1. + do { + let key = try ASN1.PKCS8PrivateKey(asn1Encoded: bytes) + self = try .init(rawRepresentation: key.privateKey.privateKey) + } catch { + let key = try ASN1.SEC1PrivateKey(asn1Encoded: bytes) + self = try .init(rawRepresentation: key.privateKey) + } + } + + init() { + self.init(wrapped: .init()) + } +} + +// MARK: Serialize +extension K1.PrivateKeyImpl { + + /// A raw representation of the private key. + var rawRepresentation: Data { + Data(wrapped.secureBytes.bytes) + } + + /// A Distinguished Encoding Rules (DER) encoded representation of the private key. + var derRepresentation: Data { + let pkey = ASN1.PKCS8PrivateKey( + algorithm: .secp256k1, + privateKey: Array(self.rawRepresentation), + publicKey: Array(self.publicKey.x963Representation) + ) + var serializer = ASN1.Serializer() + + // Serializing these keys can't throw + try! serializer.serialize(pkey) + return Data(serializer.serializedBytes) + } + + /// A Privacy-Enhanced Mail (PEM) representation of the private key. + var pemRepresentation: String { + let pemDocument = ASN1.PEMDocument(type: Self.pemType, derBytes: self.derRepresentation) + return pemDocument.pemString + } + + /// An ANSI x9.63 representation of the private key. + var x963Representation: Data { + // The x9.63 private key format is a discriminator byte (0x4) concatenated with the X and Y points + // of the key, and the K value of the secret scalar. Let's load that in. + var bytes = Data() + bytes.reserveCapacity(Self.x963ByteCount) + bytes.append(contentsOf: publicKey.x963Representation) + bytes.append(self.rawRepresentation) + return bytes + } + + + static let rawByteCount = Curve.Field.byteCount + static let x963ByteCount = K1.PublicKeyImpl.x963ByteCount + K1.PrivateKeyImpl.rawByteCount +} + +extension K1.PrivateKeyImpl { + static let pemType = "PRIVATE KEY" +} + + +// MARK: - Equatable +extension K1.PrivateKeyImpl { + /// Constant-time comparision. + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.wrapped.secureBytes == rhs.wrapped.secureBytes + } +} + +// MARK: - Hashable +extension K1.PrivateKeyImpl { + /// We use the key of the private key as input to hash + func hash(into hasher: inout Hasher) { + hasher.combine(self.publicKey) + } +} diff --git a/Sources/K1/K1/Keys/PrivateKey/PrivateKey/PrivateKey+Bridge+To+C.swift b/Sources/K1/K1/Keys/PrivateKey/PrivateKey/PrivateKey+Bridge+To+C.swift deleted file mode 100644 index 7bbfa02..0000000 --- a/Sources/K1/K1/Keys/PrivateKey/PrivateKey/PrivateKey+Bridge+To+C.swift +++ /dev/null @@ -1,639 +0,0 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-27. -// - -import secp256k1 -import CryptoKit -import Foundation - -extension Bridge { - - /// Produces a **recoverable** ECDSA signature. - static func ecdsaSignRecoverable( - message: [UInt8], - privateKey: SecureBytes, - mode: ECDSASignatureNonRecoverable.SigningMode - ) throws -> Data { - - guard message.count == K1.Curve.Field.byteCount else { - throw K1.Error.incorrectByteCountOfMessageToECDSASign - } - - if let nonceFunctionArbitraryData = mode.nonceFunctionArbitraryData { - guard nonceFunctionArbitraryData.count == 32 else { - throw K1.Error.incorrectByteCountOfArbitraryDataForNonceFunction - } - } - - var nonceFunctionArbitraryBytes: [UInt8]? = nil - if let nonceFunctionArbitraryData = mode.nonceFunctionArbitraryData { - guard nonceFunctionArbitraryData.count == K1.Curve.Field.byteCount else { - throw K1.Error.incorrectByteCountOfArbitraryDataForNonceFunction - } - nonceFunctionArbitraryBytes = [UInt8](nonceFunctionArbitraryData) - } - - var signatureRecoverableBridgedToC = secp256k1_ecdsa_recoverable_signature() - - try Self.call( - ifFailThrow: .failedToECDSASignDigest - ) { context in - secp256k1_ecdsa_sign_recoverable( - context, - &signatureRecoverableBridgedToC, - message, - privateKey.backing.bytes, - secp256k1_nonce_function_rfc6979, - nonceFunctionArbitraryBytes - ) - } - - return Data( - bytes: &signatureRecoverableBridgedToC.data, - count: MemoryLayout.size(ofValue: signatureRecoverableBridgedToC.data) - ) - } - - /// Produces a **non recoverable** ECDSA signature. - static func ecdsaSignNonRecoverable( - message: [UInt8], - privateKey: SecureBytes, - mode: ECDSASignatureNonRecoverable.SigningMode - ) throws -> Data { - - guard message.count == K1.Curve.Field.byteCount else { - throw K1.Error.incorrectByteCountOfMessageToECDSASign - } - - if let nonceFunctionArbitraryData = mode.nonceFunctionArbitraryData { - guard nonceFunctionArbitraryData.count == 32 else { - throw K1.Error.incorrectByteCountOfArbitraryDataForNonceFunction - } - } - - var nonceFunctionArbitraryBytes: [UInt8]? = nil - if let nonceFunctionArbitraryData = mode.nonceFunctionArbitraryData { - guard nonceFunctionArbitraryData.count == K1.Curve.Field.byteCount else { - throw K1.Error.incorrectByteCountOfArbitraryDataForNonceFunction - } - nonceFunctionArbitraryBytes = [UInt8](nonceFunctionArbitraryData) - } - - var signatureBridgedToC = secp256k1_ecdsa_signature() - - try Self.call( - ifFailThrow: .failedToECDSASignDigest - ) { context in - secp256k1_ecdsa_sign( - context, - &signatureBridgedToC, - message, - privateKey.backing.bytes, - secp256k1_nonce_function_rfc6979, - nonceFunctionArbitraryBytes - ) - } - - return Data( - bytes: &signatureBridgedToC.data, - count: MemoryLayout.size(ofValue: signatureBridgedToC.data) - ) - } - - static func schnorrSign( - message: [UInt8], - privateKey: SecureBytes, - input: SchnorrInput? - ) throws -> Data { - guard message.count == K1.Curve.Field.byteCount else { - throw K1.Error.failedToSchnorrSignMessageInvalidLength - } - var signatureOut = [UInt8](repeating: 0, count: 64) - - var keyPair = secp256k1_keypair() - - try Self.call( - ifFailThrow: .failedToInitializeKeyPairForSchnorrSigning - ) { context in - secp256k1_keypair_create(context, &keyPair, privateKey.backing.bytes) - } - - var auxilaryRandomBytes: [UInt8]? = nil - if let auxilaryRandomData = input?.auxilaryRandomData { - guard auxilaryRandomData.count == K1.Curve.Field.byteCount else { - throw K1.Error.failedToSchnorrSignDigestProvidedRandomnessInvalidLength - } - auxilaryRandomBytes = [UInt8](auxilaryRandomData) - } - - try Self.call( - ifFailThrow: .failedToSchnorrSignDigest - ) { context in - secp256k1_schnorrsig_sign32( - context, - &signatureOut, - message, - &keyPair, - auxilaryRandomBytes - ) - } - - var publicKey = secp256k1_xonly_pubkey() - - try Self.call( - ifFailThrow: .failedToSchnorrSignErrorGettingPubKeyFromKeyPair - ) { context in - secp256k1_keypair_xonly_pub(context, &publicKey, nil, &keyPair) - } - - try Self.call( - ifFailThrow: .failedToSchnorrSignDigestDidNotPassVerification - ) { context in - secp256k1_schnorrsig_verify(context, &signatureOut, message, message.count, &publicKey) - } - - return Data(signatureOut) - } - - enum ECDHSerializeFunction { - - /// Using the `libsecp256k1` default behaviour, which is to SHA256 hash the compressed public key - case libsecp256kDefault - - /// Following the [ANSI X9.63][ansix963] standard - /// - /// [ansix963]: https://webstore.ansi.org/standards/ascx9/ansix9632011r2017 - case ansiX963 - - /// Following no standard at all, does not hash the shared public point, and returns it in full. - case noHashWholePoint - - func hashfp() -> (Optional<@convention(c) (Optional>, Optional>, Optional>, Optional) -> Int32>) { - switch self { - case .libsecp256kDefault: return secp256k1_ecdh_hash_function_default - case .ansiX963: return ecdh_skip_hash_extract_only_x - case .noHashWholePoint: return ecdh_skip_hash_extract_x_and_y - } - } - - var outputByteCount: Int { - switch self { - case .libsecp256kDefault: return K1.Curve.Field.byteCount - case .ansiX963: return K1.Curve.Field.byteCount - case .noHashWholePoint: return K1.Format.uncompressed.length - } - } - } - - static func ecdh( - publicKey publicKeyBytes: [UInt8], - privateKey: SecureBytes, - hashFp: ECDHSerializeFunction - ) throws -> Data { - - var publicKeyBridgedToC = secp256k1_pubkey() - - try Self.call(ifFailThrow: .incorrectByteCountOfPublicKey(providedByteCount: publicKeyBytes.count)) { context in - /* Parse a variable-length public key into the pubkey object. */ - secp256k1_ec_pubkey_parse( - context, - &publicKeyBridgedToC, - publicKeyBytes, - publicKeyBytes.count - ) - } - - var sharedPublicPointBytes = [UInt8]( - repeating: 0, - count: hashFp.outputByteCount - ) - - try Self.call( - ifFailThrow: .failedToPerformDiffieHellmanKeyExchange - ) { context in - /** Compute an EC Diffie-Hellman secret in constant time - */ - secp256k1_ecdh( - context, - &sharedPublicPointBytes, // output - &publicKeyBridgedToC, // pubkey - privateKey.backing.bytes, // seckey - hashFp.hashfp(), // hashfp - nil // arbitrary data pointer that is passed through to hashfp - ) - } - return Data(sharedPublicPointBytes) - } - - static func convertToNonRecoverable( - ecdsaSignature: ECDSASignatureRecoverable - ) throws -> ECDSASignatureNonRecoverable { - var recoverableBridgedToC = secp256k1_ecdsa_recoverable_signature() - - withUnsafeMutableBytes(of: &recoverableBridgedToC.data) { pointer in - pointer.copyBytes( - from: ecdsaSignature.rawRepresentation.prefix(pointer.count) - ) - } - - var nonRecoverableBridgedToC = secp256k1_ecdsa_signature() - - try Self.call( - ifFailThrow: .failedToConvertRecoverableSignatureToNonRecoverable - ) { context in - secp256k1_ecdsa_recoverable_signature_convert( - context, - &nonRecoverableBridgedToC, - &recoverableBridgedToC - ) - } - - let signatureData = Data( - bytes: &nonRecoverableBridgedToC.data, - count: MemoryLayout.size(ofValue: nonRecoverableBridgedToC.data) - ) - - return try ECDSASignatureNonRecoverable(rawRepresentation: signatureData) - } - - /// Recover an ECDSA public key from a non recoverable signature using recovery ID - static func recoverPublicKey( - ecdsaSignature: ECDSASignatureNonRecoverable, - recoveryID: Int32, - message: [UInt8] - ) throws -> [UInt8] { - try _recoverPublicKey( - rs: ecdsaSignature.compactRepresentation(), - recoveryID: recoveryID, - message: message - ) - } - - /// Recover an ECDSA public key from a signature. - static func recoverPublicKey( - ecdsaSignature: ECDSASignatureRecoverable, - message: [UInt8] - ) throws -> [UInt8] { - var recoverableBridgedToC = secp256k1_ecdsa_recoverable_signature() - - withUnsafeMutableBytes(of: &recoverableBridgedToC.data) { pointer in - pointer.copyBytes( - from: ecdsaSignature.rawRepresentation.prefix(pointer.count) - ) - } - - return try __recoverPubKeyFrom( - signatureBridgedToC: recoverableBridgedToC, - message: message - ) - } - - /// Recover an ECDSA public key from a signature. - static func _recoverPublicKey( - rs rsData: Data, - recoveryID: Int32, - message: [UInt8] - ) throws -> [UInt8] { - var signatureBridgedToC = secp256k1_ecdsa_recoverable_signature() - let rs = [UInt8](rsData) - - try Self.call( - ifFailThrow: .failedToParseRecoverableSignatureFromECDSASignature - ) { context in - secp256k1_ecdsa_recoverable_signature_parse_compact( - context, - &signatureBridgedToC, - rs, - recoveryID - ) - } - - return try __recoverPubKeyFrom( - signatureBridgedToC: signatureBridgedToC, - message: message - ) - } - - static func __recoverPubKeyFrom( - signatureBridgedToC: secp256k1_ecdsa_recoverable_signature, - message: [UInt8] - ) throws -> [UInt8] { - var signatureBridgedToC = signatureBridgedToC - var publicKeyBridgedToC = secp256k1_pubkey() - try Self.call( - ifFailThrow: .failedToRecoverPublicKeyFromSignature - ) { context in - secp256k1_ecdsa_recover( - context, - &publicKeyBridgedToC, - &signatureBridgedToC, - message - ) - } - let publicKeyFormat = K1.Format.uncompressed - var publicPointBytes = [UInt8]( - repeating: 0, - count: publicKeyFormat.length - ) - var pubkeyBytesSerializedCount = publicKeyFormat.length - try publicPointBytes.withUnsafeMutableBytes { pubkeyBytes in - try Self.call( - ifFailThrow: .failedToSerializePublicKeyIntoBytes - ) { context in - secp256k1_ec_pubkey_serialize( - context, - pubkeyBytes.baseAddress!, - &pubkeyBytesSerializedCount, - &publicKeyBridgedToC, - publicKeyFormat.rawValue - ) - } - } - guard - pubkeyBytesSerializedCount == K1.Format.uncompressed.length, - publicPointBytes.count == K1.Format.uncompressed.length - else { - throw K1.Error.failedToSerializePublicKeyIntoBytes - } - - return publicPointBytes - } -} - - -public struct SchnorrInput { - public let auxilaryRandomData: Data -} - -public extension ECDSASignatureRecoverable { - - /// `recoverID` is optional since `self` can contain the recoveryID already. - func recoverPublicKey( - messageThatWasSigned: D - ) throws -> K1.PublicKey { - let uncompressedPublicKeyBytes = try Bridge.recoverPublicKey( - ecdsaSignature: self, - message: [UInt8](messageThatWasSigned) - ) - let publicKey = try K1.PublicKey( - wrapped: .init(uncompressedRaw: uncompressedPublicKeyBytes) - ) - - guard try publicKey.isValid(signature: self.nonRecoverable(), hashed: messageThatWasSigned) else { - throw K1.Error.expectedPublicKeyToBeValidForSignatureAndMessage - } - - return publicKey - } -} - -public extension ECDSASignatureNonRecoverable { - - /// `recoverID` is optional since `self` can contain the recoveryID already. - func recoverPublicKey( - recoveryID: Int, - messageThatWasSigned: D - ) throws -> K1.PublicKey { - let uncompressedPublicKeyBytes = try Bridge.recoverPublicKey( - ecdsaSignature: self, - recoveryID: Int32(recoveryID), - message: [UInt8](messageThatWasSigned) - ) - let publicKey = try K1.PublicKey( - wrapped: .init(uncompressedRaw: uncompressedPublicKeyBytes) - ) - - guard try publicKey.isValid(signature: self, hashed: messageThatWasSigned) else { - throw K1.Error.expectedPublicKeyToBeValidForSignatureAndMessage - } - - return publicKey - } -} - -public extension K1.PrivateKey { - - /// Produces a **recoverable** ECDSA signature. - func ecdsaSignRecoverable( - hashed message: D, - mode: ECDSASignatureNonRecoverable.SigningMode = .default - ) throws -> ECDSASignatureRecoverable { - let messageBytes = [UInt8](message) - let raw = try withSecureBytes { - try Bridge.ecdsaSignRecoverable(message: messageBytes, privateKey: $0, mode: mode) - } - - return try ECDSASignatureRecoverable.init(rawRepresentation: raw) - } - - /// Produces a **non recoverable** ECDSA signature. - func ecdsaSignNonRecoverable( - hashed message: D, - mode: ECDSASignatureNonRecoverable.SigningMode = .default - ) throws -> ECDSASignatureNonRecoverable { - let messageBytes = [UInt8](message) - let signatureData = try withSecureBytes { (secureBytes: SecureBytes) -> Data in - try Bridge.ecdsaSignNonRecoverable(message: messageBytes, privateKey: secureBytes, mode: mode) - } - - return try ECDSASignatureNonRecoverable( - rawRepresentation: signatureData - ) - } - - func schnorrSign( - hashed: D, - input maybeInput: SchnorrInput? = nil - ) throws -> SchnorrSignature { - let message = [UInt8](hashed) - let signatureData = try withSecureBytes { (secureBytes: SecureBytes) -> Data in - try Bridge.schnorrSign(message: message, privateKey: secureBytes, input: maybeInput) - } - - return try SchnorrSignature( - rawRepresentation: signatureData - ) - } - - func ecdsaSignNonRecoverable( - digest: D, - mode: ECDSASignatureNonRecoverable.SigningMode = .default - ) throws -> ECDSASignatureNonRecoverable { - try ecdsaSignNonRecoverable(hashed: Array(digest), mode: mode) - } - - func ecdsaSignNonRecoverable( - unhashed data: D, - mode: ECDSASignatureNonRecoverable.SigningMode = .default - ) throws -> ECDSASignatureNonRecoverable { - try ecdsaSignNonRecoverable(digest: SHA256.hash(data: data), mode: mode) - } - - func ecdsaSignRecoverable( - digest: D, - mode: ECDSASignatureNonRecoverable.SigningMode = .default - ) throws -> ECDSASignatureRecoverable { - try ecdsaSignRecoverable(hashed: Array(digest), mode: mode) - } - - func ecdsaSignRecoverable( - unhashed data: D, - mode: ECDSASignatureNonRecoverable.SigningMode = .default - ) throws -> ECDSASignatureRecoverable { - try ecdsaSignRecoverable(digest: SHA256.hash(data: data), mode: mode) - } - - - func schnorrSign( - digest: D, - input maybeInput: SchnorrInput? = nil - ) throws -> SchnorrSignature { - try schnorrSign(hashed: Array(digest), input: maybeInput) - } - - func schnorrSign( - unhashed data: D, - input maybeInput: SchnorrInput? = nil - ) throws -> SchnorrSignature { - try schnorrSign(digest: SHA256.hash(data: data), input: maybeInput) - } - - - func sign( - hashed: D, - scheme: S.Type, - mode: S.Signature.SigningMode - ) throws -> S.Signature { - try S.Signature.by(signing: hashed, with: self, mode: mode) - } - - func sign( - digest: S.HashDigest, - scheme: S.Type, - mode: S.Signature.SigningMode - ) throws -> S.Signature { - try S.Signature.by(signing: Array(digest), with: self, mode: mode) - } - - func sign( - unhashed: D, - scheme: S.Type, - mode: S.Signature.SigningMode - ) throws -> S.Signature { - try sign( - hashed: Data(S.hash(unhashed: unhashed)), - scheme: scheme, - mode: mode - ) - } -} - -/// MARK: ECDH -extension K1.PrivateKey { - - private func _ecdh( - publicKey: K1.PublicKey, - serializeOutputFunction hashFp: Bridge.ECDHSerializeFunction - ) throws -> Data { - let sharedSecretData = try withSecureBytes { secureBytes in - try Bridge.ecdh( - publicKey: publicKey.uncompressedRaw, - privateKey: secureBytes, - hashFp: hashFp - ) - } - return sharedSecretData - } - - /// Computes a shared secret with the provided public key from another party, - /// returning only the `X` coordinate of the point, following [ANSI X9.63][ansix963] standards. - /// - /// This is one of three ECDH functions, this library vendors, all three versions - /// uses different serialization of the shared EC Point, specifically: - /// 1. `ASN1 x9.63`: No hash, return only the `X` coordinate of the point <- this function - /// 2. `libsecp256k1`: SHA-256 hash the compressed point - /// 3. Custom: No hash, return point uncompressed - /// - /// This function uses 3. i.e. no hash, and returns only the `X` coordinate of the point. - /// This is following the [ANSI X9.63][ansix963] standard serialization of the shared point. - /// - /// Further more this function is compatible with CryptoKit, since it returns a CryptoKit - /// `SharedSecret` struct, thus offering you to use all of CryptoKit's Key Derivation Functions - /// (`KDF`s), which can be called on the `SharedSecret`. - /// - /// As seen on [StackExchange][cryptostackexchange], this version is compatible with the following - /// libraries: - /// - JS: `elliptic` (v6.4.0 in nodeJS v8.2.1) - /// - JS: `crypto` (builtin) - uses openssl under the hood (in nodeJS v8.2.1) - /// - .NET: `BouncyCastle` (BC v1.8.1.3, .NET v2.1.4) - /// - Python: pyca/cryptography (hazmat) - /// - /// [ansix963]: https://webstore.ansi.org/standards/ascx9/ansix9632011r2017 - /// [cryptostackexchange]: https://crypto.stackexchange.com/a/57727 - public func sharedSecretFromKeyAgreement( - with publicKeyShare: K1.PublicKey - ) throws -> SharedSecret { - let sharedSecretData = try _ecdh(publicKey: publicKeyShare, serializeOutputFunction: .ansiX963) - let __sharedSecret = __SharedSecret(ss: .init(bytes: sharedSecretData)) - let sharedSecret = unsafeBitCast(__sharedSecret, to: SharedSecret.self) - guard sharedSecret.withUnsafeBytes({ Data($0).count == sharedSecretData.count }) else { - throw K1.Error.failedToProduceSharedSecret - } - return sharedSecret - } - - /// Computes a shared secret with the provided public key from another party, - /// using `libsecp256k1` default behaviour, returning a hashed of the compressed point. - /// - /// This is one of three ECDH functions, this library vendors, all three versions - /// uses different serialization of the shared EC Point, specifically: - /// 1. `ASN1 x9.63`: No hash, return only the `X` coordinate of the point - /// 2. `libsecp256k1`: SHA-256 hash the compressed point <- this function - /// 3. Custom: No hash, return point uncompressed - /// - /// This function uses 1. i.e.SHA-256 hash the compressed point. - /// This is using the [default behaviour of `libsecp256k1`][libsecp256k1], which does not adhere to any - /// other standard. - /// - /// As seen on [StackExchange][cryptostackexchange], this version is compatible with all - /// libraries which wraps `libsecp256k1`, e.g.: - /// - Python wrapper: secp256k1 (v0.13.2, for python 3.6.4) - /// - JS wrapper: secp256k1 (v3.5.0, for nodeJS v8.2.1) - /// - /// [libsecp256k1]: https://github.com/bitcoin-core/secp256k1/blob/master/src/modules/ecdh/main_impl.h#L27 - /// [cryptostackexchange]: https://crypto.stackexchange.com/a/57727 - /// - public func ecdh(with publicKey: K1.PublicKey) throws -> Data { - try _ecdh(publicKey: publicKey, serializeOutputFunction: .libsecp256kDefault) - } - - - /// Computes a shared secret with the provided public key from another party, - /// returning an uncompressed public point, unhashed. - /// - /// This is one of three ECDH functions, this library vendors, all three versions - /// uses different serialization of the shared EC Point, specifically: - /// 1. `ASN1 x9.63`: No hash, return only the `X` coordinate of the point - /// 2. `libsecp256k1`: SHA-256 hash the compressed point - /// 3. Custom: No hash, return point uncompressed <- this function - /// - /// This function uses 2. i.e. no hash, return point uncompressed - /// **This is not following any standard at all**, but might be useful if you want to write your - /// cryptographic functions, e.g. some ECIES scheme. - /// - public func ecdhPoint(with publicKey: K1.PublicKey) throws -> Data { - try _ecdh(publicKey: publicKey, serializeOutputFunction: .noHashWholePoint) - } - -} - -// MUST match https://github.com/apple/swift-crypto/blob/main/Sources/Crypto/Key%20Agreement/DH.swift#L34 - -/// A Key Agreement Result -/// A SharedSecret has to go through a Key Derivation Function before being able to use by a symmetric key operation. -public struct __SharedSecret { - var ss: SecureBytes -} diff --git a/Sources/K1/K1/Keys/PrivateKey/PrivateKey/PrivateKey.swift b/Sources/K1/K1/Keys/PrivateKey/PrivateKey/PrivateKey.swift deleted file mode 100644 index f94cea3..0000000 --- a/Sources/K1/K1/Keys/PrivateKey/PrivateKey/PrivateKey.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-27. -// - -import CryptoKit -import Foundation - -// MARK: - PrivateKey -// MARK: - - -public extension K1 { - - struct PrivateKey: Sendable, Hashable { - - private let wrapped: Wrapped - - public let publicKey: PublicKey - - internal init(wrapped: Wrapped) { - self.wrapped = wrapped - self.publicKey = PublicKey(wrapped: wrapped.publicKey) - } - } -} -public extension K1.PrivateKey { - /// WARNING only use this if you really know what you are doing. This - /// exposes the private key in raw form. Potentially devastatingly dangerous. - var rawRepresentation: Data { - withSecureBytes { Data($0) } - } -} - -internal extension K1.PrivateKey { - - func withSecureBytes(function: @escaping (SecureBytes) throws -> T) rethrows -> T { - try wrapped.withSecureBytes(function: function) - } -} - -// MARK: - Conveninence Init -// MARK: - -public extension K1.PrivateKey { - - static func generateNew() throws -> Self { - let wrapped = try Wrapped.generateNew() - return Self(wrapped: wrapped) - } - - static func `import`( - rawRepresentation: D - ) throws -> Self { - - let wrapped = try Wrapped.import( - from: rawRepresentation - ) - - return Self(wrapped: wrapped) - } -} - -// MARK: - Equatable -// MARK: - -public extension K1.PrivateKey { - /// Two PrivateKey are considered equal if their PublicKeys are equal - static func == (lhs: Self, rhs: Self) -> Bool { - lhs.publicKey == rhs.publicKey - } -} - -// MARK: - Hashable -// MARK: - -public extension K1.PrivateKey { - /// We use the public key of the private key as input to hash - func hash(into hasher: inout Hasher) { - hasher.combine(self.publicKey) - } -} diff --git a/Sources/K1/K1/Keys/PrivateKey/PrivateKeyOf+Feature.swift b/Sources/K1/K1/Keys/PrivateKey/PrivateKeyOf+Feature.swift new file mode 100644 index 0000000..bedec43 --- /dev/null +++ b/Sources/K1/K1/Keys/PrivateKey/PrivateKeyOf+Feature.swift @@ -0,0 +1,62 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-24. +// + +import Foundation + +// TODO: Replace typealiases and this single existential to use existentials generated with GYB. +public struct PrivateKeyOf: Sendable, Hashable, K1PrivateKeyProtocol { + public init() { + self.init(impl: .init()) + } + + public init(rawRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(rawRepresentation: rawRepresentation)) + } + + public init(x963Representation: some ContiguousBytes) throws { + try self.init(impl: .init(x963Representation: x963Representation)) + } + + public init(derRepresentation: some RandomAccessCollection) throws { + try self.init(impl: .init(derRepresentation: derRepresentation)) + } + + public init(pemRepresentation: String) throws { + try self.init(impl: .init(pemRepresentation: pemRepresentation)) + } + + public var rawRepresentation: Data { + impl.rawRepresentation + } + + public var x963Representation: Data { + impl.x963Representation + } + + public var derRepresentation: Data { + impl.derRepresentation + } + + public var pemRepresentation: String { + impl.pemRepresentation + } + + typealias Impl = K1.PrivateKeyImpl + internal let impl: Impl + internal let publicKeyImpl: K1.PublicKeyImpl + + public typealias PublicKey = Feature.PublicKey + + public var publicKey: PublicKey { + try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) + } + + init(impl: Impl) { + self.impl = impl + self.publicKeyImpl = impl.publicKey + } +} diff --git a/Sources/K1/K1/Keys/PrivateKey/Wrapped/PrivateKey+Wrapped.swift b/Sources/K1/K1/Keys/PrivateKey/Wrapped/PrivateKey+Wrapped.swift deleted file mode 100644 index f3c0709..0000000 --- a/Sources/K1/K1/Keys/PrivateKey/Wrapped/PrivateKey+Wrapped.swift +++ /dev/null @@ -1,120 +0,0 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-27. -// - -import Foundation - - -internal extension K1.PrivateKey { - - @usableFromInline - struct Wrapped: @unchecked Sendable { - - private let secureBytes: SecureBytes - - /// Allowing for "lazy" computation of public key. This is a workaround - /// the fact that (immutable instances of) structs cannot have lazy - /// properties. - private let _publicKey: MemoizationBox = .init() - - private init( - secureBytes: SecureBytes - ) throws { - do { - let _ = try K1.PublicKey.Wrapped.derive(privateKeyBytes: secureBytes.bytes) - self.secureBytes = secureBytes - - } catch { - if secureBytes.first == 0x00 { - throw K1.Error.invalidPrivateKeyMustNotBeZero - } else { - throw K1.Error.invalidPrivateKeyMustBeSmallerThanOrder - } - } - - - } - } - -} - - -internal extension K1.PrivateKey.Wrapped { - - /// The computed public key of this private key. - @usableFromInline - var publicKey: K1.PublicKey.Wrapped { - _publicKey.getOrEvaluate { - do { - return try K1.PublicKey.Wrapped.derive(privateKeyBytes: secureBytes.bytes) - } catch { - fatalError("Should always be able to derive public key from private key, but got underlying error: \(error)") - } - } - } - - static func `import`( - from privateKeyBytes: [UInt8] - ) throws -> Self { - - guard - privateKeyBytes.count == K1.PrivateKey.Wrapped.byteCount - else { - throw K1.Error.invalidSizeOfPrivateKey(providedByteCount: privateKeyBytes.count) - } - return try .init(secureBytes: SecureBytes(privateKeyBytes)) - } - - static func `import`( - from data: D - ) throws -> Self { - try .import( - from: data.bytes - ) - } - - /// Generate a new private key from randomness. - @usableFromInline - static func generateNew() throws -> Self { - - var attempt = 0 - - while attempt < 100 { - defer { attempt += 1 } - do { - let secureBytes = SecureBytes(count: Self.byteCount) - let privateKey = try Self(secureBytes: secureBytes) - // Success => return valid private key - return privateKey - } catch { - // Failure (due to unlikely scenario that the private key scalar > order of the curve) => retry - } - } - - // Probability of this happening is: - // n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - // (n / 2^256) ^ 100 = lim 0 - // I.e. will not happen. - fatalError(""" - Failed to generate private key after #\(attempt) attempts. - You are the most unlucky person in the universe. - Or by Occam's razor: the person writing this code made some error. - """ - ) - } -} - -internal extension K1.PrivateKey.Wrapped { - - func withSecureBytes(function: (SecureBytes) throws -> T) rethrows -> T { - try function(secureBytes) - } -} - -internal extension K1.PrivateKey.Wrapped { - static let byteCount = K1.Curve.Field.byteCount - -} diff --git a/Sources/K1/K1/Keys/PublicKey/PublicKey.swift b/Sources/K1/K1/Keys/PublicKey/PublicKey.swift new file mode 100644 index 0000000..b1a3a9f --- /dev/null +++ b/Sources/K1/K1/Keys/PublicKey/PublicKey.swift @@ -0,0 +1,147 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2022-01-27. +// + +import Foundation + +public protocol K1PublicKeyProtocol: K1KeyPortable { + init(compressedRepresentation: some ContiguousBytes) throws + var compressedRepresentation: Data { get } +} + +extension K1 { + + struct PublicKeyImpl: Sendable, Hashable, K1PublicKeyProtocol { + + typealias Wrapped = FFI.PublicKey.Wrapped + internal let wrapped: Wrapped + + internal init(wrapped: Wrapped) { + self.wrapped = wrapped + } + } +} + +// MARK: Init +extension K1.PublicKeyImpl { + + + /// `04 || X || Y` (65 bytes) + static let x963ByteCount = FFI.PublicKey.x963ByteCount + + /// `X || Y` (64 bytes) + static let rawByteCount = FFI.PublicKey.rawByteCount + + /// `02|03 || X` (33 bytes) + static let compressedByteCount = FFI.PublicKey.compressedByteCount + + /// `X || Y` (64 bytes) + init(rawRepresentation: some ContiguousBytes) throws { + try self.init( + wrapped: FFI.PublicKey.deserialize(rawRepresentation: rawRepresentation) + ) + } + + /// `04 || X || Y` (65 bytes) + init(x963Representation: some ContiguousBytes) throws { + try self.init( + wrapped: FFI.PublicKey.deserialize(x963Representation: x963Representation) + ) + } + + /// `DER` + init(derRepresentation: some RandomAccessCollection) throws { + let bytes = [UInt8](derRepresentation) + let parsed = try ASN1.SubjectPublicKeyInfo(asn1Encoded: bytes) + self = try .init(x963Representation: parsed.key) + } + + /// `02|03 || X` (33 bytes) + init(compressedRepresentation: some ContiguousBytes) throws { + try self.init( + wrapped: FFI.PublicKey.deserialize(compressedRepresentation: compressedRepresentation) + ) + } + + /// Creates a `secp256k1` key from a Privacy-Enhanced Mail (PEM) representation. + init(pemRepresentation: String) throws { + let pem = try ASN1.PEMDocument(pemString: pemRepresentation) + guard pem.type == Self.pemType else { + throw K1.Error.invalidPEMDocument + } + self = try .init(derRepresentation: pem.derBytes) + } + +} + +extension K1.PublicKeyImpl { + static let pemType = "PUBLIC KEY" +} + +// MARK: Serialize +extension K1.PublicKeyImpl { + + /// `X || Y` (64 bytes) + var rawRepresentation: Data { + Data(x963Representation.dropFirst()) + } + + + /// `04 || X || Y` (65 bytes) + var x963Representation: Data { + try! FFI.PublicKey.serialize(wrapped, format: .uncompressed) + } + + /// `02|03 || X` (33 bytes) + var compressedRepresentation: Data { + try! FFI.PublicKey.serialize(wrapped, format: .compressed) + } + + /// `DER` + var derRepresentation: Data { + let spki = ASN1.SubjectPublicKeyInfo( + algorithmIdentifier: .secp256k1, + key: Array(self.x963Representation) + ) + var serializer = ASN1.Serializer() + + // Serializing these keys can't throw + try! serializer.serialize(spki) + return Data(serializer.serializedBytes) + } + + /// A Privacy-Enhanced Mail (PEM) representation of the public key. + public var pemRepresentation: String { + let pemDocument = ASN1.PEMDocument(type: Self.pemType, derBytes: self.derRepresentation) + return pemDocument.pemString + } +} + +// MARK: Equatable +extension K1.PublicKeyImpl { + static func == (lhsSelf: Self, rhsSelf: Self) -> Bool { + let lhs = lhsSelf.wrapped + let rhs = rhsSelf.wrapped + do { + return try lhs.compare(to: rhs) + } catch { + return lhs.withUnsafeBytes { lhsBytes in + rhs.withUnsafeBytes { rhsBytes in + safeCompare(lhsBytes, rhsBytes) + } + } + } + } +} + +// MARK: Hashable +extension K1.PublicKeyImpl { + func hash(into hasher: inout Hasher) { + wrapped.withUnsafeBytes { + hasher.combine(bytes: $0) + } + } +} diff --git a/Sources/K1/K1/Keys/PublicKey/PublicKey/PublicKey+Bridge+To+C.swift b/Sources/K1/K1/Keys/PublicKey/PublicKey/PublicKey+Bridge+To+C.swift deleted file mode 100644 index bcee89d..0000000 --- a/Sources/K1/K1/Keys/PublicKey/PublicKey/PublicKey+Bridge+To+C.swift +++ /dev/null @@ -1,423 +0,0 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-28. -// - - -// Bridge to C -import secp256k1 -import CryptoKit -import Foundation - -// MARK: - Validate (Verify) -// MARK: - -internal extension K1.PublicKey { - - func isValidSchnorrSignature( - _ signature: SchnorrSignature, - hashed messageData: D - ) throws -> Bool { - let message = [UInt8](messageData) - guard message.count == K1.Curve.Field.byteCount else { - throw K1.Error.incorrectByteCountOfMessageToValidate - } - return try Bridge.toC { bridge -> Bool in - let schnorrBytes = [UInt8](signature.rawRepresentation) - - var publicKeyBridgedToC = secp256k1_pubkey() - - try bridge.call(ifFailThrow: .failedToParsePublicKeyFromBytes) { context in - /* Parse a variable-length public key into the pubkey object. */ - secp256k1_ec_pubkey_parse( - context, - &publicKeyBridgedToC, - uncompressedRaw, - uncompressedRaw.count - ) - } - - var publicKeyX = secp256k1_xonly_pubkey() - - try bridge.call(ifFailThrow: .failedToSchnorrVerifyGettingXFromPubKey) { context in - secp256k1_xonly_pubkey_from_pubkey(context, &publicKeyX, nil, &publicKeyBridgedToC) - } - - return bridge.validate { context in - secp256k1_schnorrsig_verify( - context, - schnorrBytes, - message, - message.count, - &publicKeyX - ) - } - } - } - - func isValidECDSASignature( - _ signature: ECDSASignatureNonRecoverable, - hashed messageData: D, - mode: SignatureValidationMode = .default - ) throws -> Bool { - let message = [UInt8](messageData) - guard message.count == K1.Curve.Field.byteCount else { - throw K1.Error.incorrectByteCountOfMessageToValidate - } - - return try Bridge.toC { bridge -> Bool in - - var publicKeyBridgedToC = secp256k1_pubkey() - - try bridge.call(ifFailThrow: .failedToParsePublicKeyFromBytes) { context in - /* Parse a variable-length public key into the pubkey object. */ - secp256k1_ec_pubkey_parse( - context, - &publicKeyBridgedToC, - uncompressedRaw, - uncompressedRaw.count - ) - } - - var signatureBridgedToCPotentiallyMalleable = secp256k1_ecdsa_signature() - withUnsafeMutableBytes(of: &signatureBridgedToCPotentiallyMalleable.data) { pointer in - pointer.copyBytes( - from: signature.rawRepresentation.prefix(pointer.count) - ) - } - - var signatureBridgedToCNonMalleable = secp256k1_ecdsa_signature() - - let codeForSignatureWasMalleable = 1 - let signatureWasMalleableResult = bridge.callWithResultCode { context in - secp256k1_ecdsa_signature_normalize( - context, - &signatureBridgedToCNonMalleable, // out - &signatureBridgedToCPotentiallyMalleable // in - ) - } - - let signatureWasMalleable = signatureWasMalleableResult == codeForSignatureWasMalleable - - let isSignatureValid = bridge.validate { context in - secp256k1_ecdsa_verify( - context, - &signatureBridgedToCNonMalleable, - message, - &publicKeyBridgedToC - ) - } - - let acceptMalleableSignatures = mode == .acceptSignatureMalleability - - switch (isSignatureValid, signatureWasMalleable, acceptMalleableSignatures) { - case (true, false, _): - // Signature is valid - return true - case (true, true, true): - // Signature was valid but malleable, since you specified to - // accept malleability => considering signature valid. - return true - case (true, true, false): - // Signature was valid, but not normalized which was required => - // considering signature invalid. - return false - case (false, _, _): - // Signature is invalid. - return false - } - } - } -} - -//public enum Schnorr: SignatureScheme {} - -// MARK: - Validate ECDSA (Verify) -// MARK: - -public extension K1.PublicKey { - - func isValidECDSASignature( - _ signature: ECDSASignatureNonRecoverable, - digest: D, - mode: SignatureValidationMode = .default - ) throws -> Bool { - try isValidECDSASignature(signature, hashed: Data(digest), mode: mode) - } - - func isValid(signature: ECSignatureBase, for digest: D) throws -> Bool { - if let schnorrSignature = signature as? SchnorrSignature { - return try isValidSchnorrSignature(schnorrSignature, digest: digest) - } else if let ecdsaSignatureRecoverable = signature as? ECDSASignatureRecoverable { - return try isValidECDSASignature(ecdsaSignatureRecoverable.nonRecoverable(), digest: digest) - } else if let ecdsaSignatureNonRecoverable = signature as? ECDSASignatureNonRecoverable { - return try isValidECDSASignature(ecdsaSignatureNonRecoverable, digest: digest) - } else { - throw K1.Error.failedToRecognizeSignatureType(onlySupportedSchemesAre: SigningScheme.allCases) - } - } - - func isValid(signature: ECSignatureBase, hashed: D) throws -> Bool { - if let schnorrSignature = signature as? SchnorrSignature { - return try isValidSchnorrSignature(schnorrSignature, hashed: hashed) - } else if let ecdsaSignatureRecoverable = signature as? ECDSASignatureRecoverable { - return try isValidECDSASignature(ecdsaSignatureRecoverable.nonRecoverable(), hashed: hashed) - } else if let ecdsaSignatureNonRecoverable = signature as? ECDSASignatureNonRecoverable { - return try isValidECDSASignature(ecdsaSignatureNonRecoverable, hashed: hashed) - } else { - throw K1.Error.failedToRecognizeSignatureType(onlySupportedSchemesAre: SigningScheme.allCases) - } - } - - func isValid(signature: ECSignatureBase, unhashed: M) throws -> Bool { - try isValid(signature: signature, for: SHA256.hash(data: unhashed)) - } - - func isValidECDSASignature( - _ signature: ECDSASignatureNonRecoverable, - unhashed: M, - mode: SignatureValidationMode = .default - ) throws -> Bool { - try isValidECDSASignature(signature, digest: SHA256.hash(data: unhashed), mode: mode) - } - - func isValidECDSASignature( - _ signature: ECDSASignatureRecoverable, - unhashed: M, - mode: SignatureValidationMode = .default - ) throws -> Bool { - try isValidECDSASignature(signature.nonRecoverable(), unhashed: unhashed, mode: mode) - } -} - -// MARK: - Validate Schnorr (Verify) -// MARK: - -public extension K1.PublicKey { - - func isValidSchnorrSignature( - _ signature: SchnorrSignature, - digest: D - ) throws -> Bool { - try isValidSchnorrSignature(signature, hashed: Data(digest)) - } - - func isValidSchnorrSignature( - _ signature: SchnorrSignature, - unhashed: M - ) throws -> Bool { - try isValidSchnorrSignature(signature, digest: SHA256.hash(data: unhashed)) - } -} - -/// Validation mode controls whether or not signature malleability should -/// is forbidden or allowed. Read more about it [here][more] -/// -/// [more]: https://github.com/bitcoin-core/secp256k1/blob/2e5e4b67dfb67950563c5f0ab2a62e25eb1f35c5/include/secp256k1.h#L510-L550 -public enum SignatureValidationMode { - case preventSignatureMalleability - case acceptSignatureMalleability -} - -public extension SignatureValidationMode { - static let `default`: Self = .acceptSignatureMalleability -} - -public protocol ECSignatureBase { - static var scheme: SigningScheme { get } - - func wasSigned( - by signer: K1.PublicKey, - for digest: D - ) throws -> Bool -} - -public protocol ECSignature: ECSignatureBase { - associatedtype Scheme: ECSignatureScheme - associatedtype ValidationMode - associatedtype SigningMode - - func wasSigned( - by signer: K1.PublicKey, - for digest: D, - mode: ValidationMode - ) throws -> Bool - - func wasSigned( - by signer: K1.PublicKey, - hashedMessage: D, - mode: ValidationMode - ) throws -> Bool - - static func by( - signing hashed: D, - with privateKey: K1.PrivateKey, - mode: SigningMode - ) throws -> Self -} - -public extension ECSignature where ValidationMode == Void { - - func wasSigned( - by signer: K1.PublicKey, - for digest: D - ) throws -> Bool { - try wasSigned(by: signer, for: digest, mode: ()) - } -} - -public extension ECSignature { - - static func bySigning( - digest: D, - with privateKey: K1.PrivateKey, - mode: SigningMode - ) throws -> Self { - try by(signing: Array(digest), with: privateKey, mode: mode) - } - - static func bySigning( - unhashed data: D, - with privateKey: K1.PrivateKey, - mode: SigningMode - ) throws -> Self { - try bySigning(digest: SHA256.hash(data: data), with: privateKey, mode: mode) - } -} - -public extension ECDSASignatureNonRecoverable { - - typealias ValidationMode = SignatureValidationMode - - struct SigningMode { - public let nonceFunctionArbitraryData: Data? - public init(nonceFunctionArbitraryData: Data? = nil) { - self.nonceFunctionArbitraryData = nonceFunctionArbitraryData - } - } - - static func by( - signing hashed: D, - with privateKey: K1.PrivateKey, - mode: SigningMode - ) throws -> Self { - try privateKey.ecdsaSignNonRecoverable(hashed: hashed, mode: mode) - } - - func wasSigned( - by signer: K1.PublicKey, - hashedMessage: D, - mode: ValidationMode = .default - ) throws -> Bool { - try signer.isValidECDSASignature(self, hashed: hashedMessage, mode: mode) - } - - func wasSigned(by signer: K1.PublicKey, for digest: D) throws -> Bool { - try wasSigned(by: signer, for: digest, mode: .default) - } - - func wasSigned( - by signer: K1.PublicKey, - for digest: D, - mode: ValidationMode = .default - ) throws -> Bool { - try signer.isValidECDSASignature( - self, - digest: digest, - mode: mode - ) - } -} - -public extension ECDSASignatureNonRecoverable.SigningMode { - static let `default`: Self = .init() -} - -public protocol ECSignatureScheme where Signature.Scheme == Self { - associatedtype Hasher: HashFunction - associatedtype Signature: ECSignature - static var scheme: SigningScheme { get } -} -public extension ECSignatureScheme { - typealias HashDigest = Hasher.Digest - static func hash(unhashed: D) throws -> HashDigest { - Hasher.hash(data: unhashed) - } -} - - -public extension ECSignatureScheme { - static var scheme: SigningScheme { Signature.scheme } -} - -public enum ECDSA: ECSignatureScheme { - public typealias Hasher = SHA256 - public typealias Signature = ECDSASignatureNonRecoverable -} - -public enum Schnorr: ECSignatureScheme { - public typealias Hasher = SHA256 - public typealias Signature = SchnorrSignature -} - -public enum SigningScheme: String, Equatable, CaseIterable { - case schnorr - case ecdsa -} - -public struct SchnorrSignature: ECSignature, Sendable, Hashable { - public typealias Scheme = Schnorr - private let _rawRepresentation: [UInt8] - public var rawRepresentation: Data { - Data(_rawRepresentation) - } - - public init(rawRepresentation: D) throws { - guard - rawRepresentation.count == 2 * K1.Curve.Field.byteCount - else { - throw K1.Error.incorrectByteCountOfRawSignature - } - - self._rawRepresentation = [UInt8](rawRepresentation) - } -} - -public extension SchnorrSignature { - static let scheme: SigningScheme = .schnorr - typealias ValidationMode = Void - - func compactRepresentation() throws -> Data { - try Bridge.compactRepresentationOfSignature(rawRepresentation: rawRepresentation) - } - func derRepresentation() throws -> Data { - try Bridge.derRepresentationOfSignature(rawRepresentation: rawRepresentation) - } - - func wasSigned( - by signer: K1.PublicKey, - for digest: D, - mode _: Void - ) throws -> Bool { - try signer.isValidSchnorrSignature(self, digest: digest) - } - - - func wasSigned( - by signer: K1.PublicKey, - hashedMessage: D, - mode: ValidationMode - ) throws -> Bool { - try signer.isValidSchnorrSignature(self, hashed: hashedMessage) - } - - typealias SigningMode = SchnorrInput - - static func by( - signing hashed: D, - with privateKey: K1.PrivateKey, - mode: SigningMode - ) throws -> Self { - try privateKey.schnorrSign(hashed: hashed, input: mode) - } - -} diff --git a/Sources/K1/K1/Keys/PublicKey/PublicKey/PublicKey.swift b/Sources/K1/K1/Keys/PublicKey/PublicKey/PublicKey.swift deleted file mode 100644 index 15841f2..0000000 --- a/Sources/K1/K1/Keys/PublicKey/PublicKey/PublicKey.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-27. -// - -import Foundation - -public extension K1 { - - struct PublicKey: Sendable, Hashable { - - private let wrapped: Wrapped - - internal init(wrapped: Wrapped) { - self.wrapped = wrapped - } - } -} - -internal extension K1.PublicKey { - var uncompressedRaw: [UInt8] { - wrapped.uncompressedRaw - } -} - -// MARK: - Convenience Init -// MARK: - -public extension K1.PublicKey { - - static func `import`( - from data: D - ) throws -> Self { - try self.init( - wrapped: .import(from: data) - ) - } - -} - -public extension K1.PublicKey { - - func rawRepresentation(format: K1.Format) throws -> [UInt8] { - try wrapped.rawRepresentation(format: format) - } - - -} - diff --git a/Sources/K1/K1/Keys/PublicKey/PublicKeyOf+Feature.swift b/Sources/K1/K1/Keys/PublicKey/PublicKeyOf+Feature.swift new file mode 100644 index 0000000..24b5613 --- /dev/null +++ b/Sources/K1/K1/Keys/PublicKey/PublicKeyOf+Feature.swift @@ -0,0 +1,58 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-24. +// + +import Foundation + +// TODO: Replace typealiases and this single existential to use existentials generated with GYB. +public struct PublicKeyOf: Sendable, Hashable, K1PublicKeyProtocol { + + public init(rawRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(rawRepresentation: rawRepresentation)) + } + + public init(compressedRepresentation: some ContiguousBytes) throws { + try self.init(impl: .init(compressedRepresentation: compressedRepresentation)) + } + + public init(x963Representation: some ContiguousBytes) throws { + try self.init(impl: .init(x963Representation: x963Representation)) + } + + public init(derRepresentation: some RandomAccessCollection) throws { + try self.init(impl: .init(derRepresentation: derRepresentation)) + } + + public init(pemRepresentation: String) throws { + try self.init(impl: .init(pemRepresentation: pemRepresentation)) + } + + public var rawRepresentation: Data { + impl.rawRepresentation + } + + public var x963Representation: Data { + impl.x963Representation + } + + public var derRepresentation: Data { + impl.derRepresentation + } + + public var compressedRepresentation: Data { + impl.compressedRepresentation + } + + public var pemRepresentation: String { + impl.pemRepresentation + } + + typealias Impl = K1.PublicKeyImpl + internal let impl: Impl + internal init(impl: Impl) { + self.impl = impl + } +} diff --git a/Sources/K1/K1/Keys/PublicKey/Wrapped/PublicKey+Wrapped+Bridge+To+C.swift b/Sources/K1/K1/Keys/PublicKey/Wrapped/PublicKey+Wrapped+Bridge+To+C.swift deleted file mode 100644 index 0f4b785..0000000 --- a/Sources/K1/K1/Keys/PublicKey/Wrapped/PublicKey+Wrapped+Bridge+To+C.swift +++ /dev/null @@ -1,170 +0,0 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-27. -// - -import secp256k1 -import Foundation - -extension Bridge { - - static func publicKeyParse( - raw: [UInt8] - ) throws -> Data { - - guard K1.Format.allCases.map(\.length).contains(raw.count) else { - throw K1.Error.incorrectByteCountOfPublicKey(providedByteCount: raw.count) - } - - var publicKeyBytesMaybeCompressed = raw - var publicKeyBridgedToC = secp256k1_pubkey() - - try Self.call( - ifFailThrow: .failedToParsePublicKeyFromBytes - ) { context in - /* Parse a variable-length public key into the pubkey object. */ - secp256k1_ec_pubkey_parse( - context, - &publicKeyBridgedToC, - &publicKeyBytesMaybeCompressed, - raw.count - ) - } - - if publicKeyBytesMaybeCompressed.count == K1.Format.uncompressed.length { - return Data(publicKeyBytesMaybeCompressed) - } - - // Was compressed, need to uncompress - - var publicKeyBytesUncompressedLength = K1.Format.uncompressed.length - var publicKeyBytesUncompressed = [UInt8].init(repeating: 0, count: publicKeyBytesUncompressedLength) - - try Self.call( - ifFailThrow: .failedToUncompressPublicKey - ) { context in - /* "Serialize a pubkey object into a serialized byte sequence." */ - secp256k1_ec_pubkey_serialize( - context, - &publicKeyBytesUncompressed, - &publicKeyBytesUncompressedLength, - &publicKeyBridgedToC, - K1.Format.uncompressed.rawValue - ) - } - - return Data(publicKeyBytesUncompressed) - - } - - static func compress( - publicKey: K1.PublicKey.Wrapped - ) throws -> Data { - - var publicKeyBridgedToC = secp256k1_pubkey() - - try Self.call(ifFailThrow: .failedToParsePublicKeyFromBytes) { context in - /* Parse a variable-length public key into the pubkey object. */ - secp256k1_ec_pubkey_parse( - context, - &publicKeyBridgedToC, - publicKey.uncompressedRaw, - publicKey.uncompressedRaw.count - ) - } - - let publicKeyFormat = K1.Format.compressed - - var publicKeyCompressedByteCount = publicKeyFormat.length - var publicKeyBytes = [UInt8]( - repeating: 0, - count: publicKeyCompressedByteCount - ) - - try Self.call( - ifFailThrow: .failedToCompressPublicKey - ) { context in - /* "Serialize a pubkey object into a serialized byte sequence." */ - secp256k1_ec_pubkey_serialize( - context, - &publicKeyBytes, - &publicKeyCompressedByteCount, - &publicKeyBridgedToC, - publicKeyFormat.rawValue - ) - } - - return Data(publicKeyBytes) - } - - - static func publicKeyCreate(privateKeyBytes: [UInt8]) throws -> Data { - - guard - privateKeyBytes.count == K1.PrivateKey.Wrapped.byteCount - else { - throw K1.Error.invalidSizeOfPrivateKey(providedByteCount: privateKeyBytes.count) - } - - let publicKeyFormat = K1.Format.uncompressed - var publicKeyByteCount = publicKeyFormat.length - var publicKeyBridgedToC = secp256k1_pubkey() - - var publicKeyBytes = [UInt8]( - repeating: 0, - count: publicKeyFormat.length - ) - - try Bridge.toC { bridge in - - try bridge.call( - ifFailThrow: .failedToUpdateContextRandomization - ) { - secp256k1_context_randomize($0, privateKeyBytes) - } - - try bridge.call( - ifFailThrow: .failedToComputePublicKeyFromPrivateKey - ) { - /* "Compute the public key for a secret key." */ - secp256k1_ec_pubkey_create($0, &publicKeyBridgedToC, privateKeyBytes) - } - - try bridge.call( - ifFailThrow: .failedToSerializePublicKeyIntoBytes - ) { - /* "Serialize a pubkey object into a serialized byte sequence." */ - secp256k1_ec_pubkey_serialize( - $0, - &publicKeyBytes, - &publicKeyByteCount, - &publicKeyBridgedToC, - publicKeyFormat.rawValue - ) - } - } - - assert(publicKeyByteCount == publicKeyFormat.length) - - return Data(publicKeyBytes) - } -} - - -internal extension K1.PublicKey.Wrapped { - - static func `import`(from raw: [UInt8]) throws -> Self { - let publicKeyBytes = try Bridge.publicKeyParse(raw: raw) - return try Self(uncompressedRaw: publicKeyBytes.bytes) - } - - static func derive( - privateKeyBytes: [UInt8] - ) throws -> Self { - let publicKeyRaw = try Bridge.publicKeyCreate(privateKeyBytes: privateKeyBytes) - return try Self(uncompressedRaw: publicKeyRaw.bytes) - } - -} diff --git a/Sources/K1/K1/Keys/PublicKey/Wrapped/PublicKey+Wrapped.swift b/Sources/K1/K1/Keys/PublicKey/Wrapped/PublicKey+Wrapped.swift deleted file mode 100644 index 75bbc06..0000000 --- a/Sources/K1/K1/Keys/PublicKey/Wrapped/PublicKey+Wrapped.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-27. -// - -import Foundation - -internal extension K1.PublicKey { - - @usableFromInline - struct Wrapped: Sendable, Hashable { - - internal let uncompressedRaw: [UInt8] - - internal init( - uncompressedRaw: [UInt8] - ) throws { - guard uncompressedRaw.count == K1.Format.uncompressed.length else { - // Only accept uncompressed public key here. - throw K1.Error.incorrectByteCountOfPublicKey(got: uncompressedRaw.count, acceptableLengths: [K1.Format.uncompressed.length]) - } - self.uncompressedRaw = uncompressedRaw - } - } -} - -internal extension K1.PublicKey.Wrapped { - - @usableFromInline - func rawRepresentation(format: K1.Format) throws -> [UInt8] { - switch format { - case .uncompressed: return uncompressedRaw - case .compressed: return try Array(Bridge.compress(publicKey: self)) - } - } - - static func `import`( - from raw: D) - throws -> Self { - try .import(from: raw.bytes) - } - - @usableFromInline - static func == (lhs: Self, rhs: Self) -> Bool { - /// We use constant time comparision to not leak any information about - /// the private key, because `PrivateKey` is `Equatable` and checks - /// equality using its publicKey. - safeCompare(lhs.uncompressedRaw, rhs.uncompressedRaw) - } -} diff --git a/Sources/K1/K1/Schnorr/Schnorr+Signature.swift b/Sources/K1/K1/Schnorr/Schnorr+Signature.swift new file mode 100644 index 0000000..4c5ca37 --- /dev/null +++ b/Sources/K1/K1/Schnorr/Schnorr+Signature.swift @@ -0,0 +1,33 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-19. +// + +import Foundation + +extension K1.Schnorr { + public struct Signature: Sendable, Hashable { + + typealias Wrapped = FFI.Schnorr.Wrapped + internal let wrapped: Wrapped + internal init(wrapped: Wrapped) { + self.wrapped = wrapped + } + } +} + +// MARK: Init +extension K1.Schnorr.Signature { + public init(rawRepresentation: some DataProtocol) throws { + try self.init(wrapped: .init(bytes: [UInt8](rawRepresentation))) + } +} + +// MARK: Serialize +extension K1.Schnorr.Signature { + public var rawRepresentation: Data { + wrapped.rawRepresentation + } +} diff --git a/Sources/K1/K1/Schnorr/Schnorr.swift b/Sources/K1/K1/Schnorr/Schnorr.swift new file mode 100644 index 0000000..c5122c5 --- /dev/null +++ b/Sources/K1/K1/Schnorr/Schnorr.swift @@ -0,0 +1,147 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-24. +// + +import Foundation +import protocol CryptoKit.Digest +import struct CryptoKit.SHA256 + +extension K1 { + + /// A mechanism used to create or verify a cryptographic signature using the `secp256k1` and Schnorr signature scheme. + public enum Schnorr: K1Feature { + + /// A `secp256k1` private key used to create cryptographic signatures, + /// more specifically Schnorr signatures. + public typealias PrivateKey = PrivateKeyOf + + /// A `secp256k1` public key used to verify cryptographic signatures, + /// more specifically Schnorr signatures. + public typealias PublicKey = PublicKeyOf + } +} + +// MARK: Sign +extension K1.Schnorr.PrivateKey { + public func signature( + for hashed: some DataProtocol, + options: K1.Schnorr.SigningOptions = .default + ) throws -> K1.Schnorr.Signature { + try K1.Schnorr.Signature( + wrapped: FFI.Schnorr.sign( + hashedMessage: [UInt8](hashed), + privateKey: impl.wrapped, + options: options + ) + ) + } + + public func signature( + for digest: some Digest, + options: K1.Schnorr.SigningOptions = .default + ) throws -> K1.Schnorr.Signature { + try signature( + for: [UInt8](digest), + options: options + ) + } + + /// SHA256 hashes `unhashed` before signing it. + public func signature( + forUnhashed unhashed: some DataProtocol, + options: K1.Schnorr.SigningOptions = .default + ) throws -> K1.Schnorr.Signature { + try signature( + for: SHA256.hash(data: unhashed), + options: options + ) + } +} + +// MARK: Validate +extension K1.Schnorr.PublicKey { + + /// Verifies a Schnorr signature on some _hash_ over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The Schnorr signature to check against the _hashed_ data. + /// - hashed: The _hashed_ data covered by the signature. + /// - Returns: A Boolean value that’s true if the signature is valid for the given _hashed_ data. + public func isValidSignature( + _ signature: K1.Schnorr.Signature, + hashed: some DataProtocol + ) -> Bool { + do { + return try FFI.Schnorr.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: [UInt8](hashed) + ) + } catch { + return false + } + } + + + /// Verifies a Schnorr signature on a digest over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The Schnorr signature to check against the given digest. + /// - digest: The digest covered by the signature. + /// - Returns: A Boolean value that’s true if the signature is valid for the given digest. + public func isValidSignature( + _ signature: K1.Schnorr.Signature, + digest: some Digest + ) -> Bool { + isValidSignature( + signature, + hashed: [UInt8](digest) + ) + } + + /// Verifies a Schnorr signature on a block of data over the `secp256k1` elliptic curve. + /// + /// The function computes an SHA-256 hash from the data before verifying the signature. If you separately hash the data to be signed, use `isValidSignature(_:digest:input)` with the created digest. Or if you have access to a digest just as `some DataProtocol`, use + /// `isValidSignature(_:hashed:input)` + /// . + /// - Parameters: + /// - signature: The Schnorr signature to check against the block of data. + /// - unhashed: The block of data covered by the signature. + /// - Returns: A Boolean value that’s true if the signature is valid for the given block of data. + public func isValidSignature( + _ signature: K1.Schnorr.Signature, + unhashed: some DataProtocol + ) -> Bool { + isValidSignature( + signature, + digest: SHA256.hash(data: unhashed) + ) + } + +} + +extension K1.Schnorr { + public struct SigningOptions: Sendable, Hashable { + public let auxilaryRandomData: AuxilaryRandomData? + public init(auxilaryRandomData: AuxilaryRandomData? = nil) { + self.auxilaryRandomData = auxilaryRandomData + } + } +} + +extension K1.Schnorr.SigningOptions { + + public static let `default` = Self() + + public struct AuxilaryRandomData: Sendable, Hashable { + public let aux: [UInt8] + + public init(aux: some DataProtocol) throws { + guard aux.count == Curve.Field.byteCount else { + throw K1.Error.failedToSchnorrSignDigestProvidedRandomnessInvalidLength + } + self.aux = [UInt8](aux) + } + } +} diff --git a/Sources/K1/Extensions/Data+Extensions.swift b/Sources/K1/Support/Extensions/Data+Extensions.swift similarity index 66% rename from Sources/K1/Extensions/Data+Extensions.swift rename to Sources/K1/Support/Extensions/Data+Extensions.swift index d97ce8f..bac5bb4 100644 --- a/Sources/K1/Extensions/Data+Extensions.swift +++ b/Sources/K1/Support/Extensions/Data+Extensions.swift @@ -8,13 +8,6 @@ import Foundation -extension Data { - var bytes: [UInt8] { - withUnsafeBytes { pointer in - Array(pointer) - } - } -} extension ContiguousBytes { diff --git a/Sources/K1/Support/FFI/API/ECDH/FFI+ECDH.swift b/Sources/K1/Support/FFI/API/ECDH/FFI+ECDH.swift new file mode 100644 index 0000000..7860103 --- /dev/null +++ b/Sources/K1/Support/FFI/API/ECDH/FFI+ECDH.swift @@ -0,0 +1,98 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-19. +// + +import Foundation +import secp256k1 + +extension FFI { + + /// Just a namespace for `FFI ECDH` + enum ECDH {} +} + +// MARK: SerializeFunction +extension FFI.ECDH { + + enum SerializeFunction { + + /// Using the `libsecp256k1` default behaviour. + /// + /// SHA256 hashes the **compressed** shared point. + /// Accepts arbitrary data passed through hash function as well. + /// + case libsecp256kDefault(arbitraryData: Data?) + + /// Following the [ANSI X9.63][ansix963] standard + /// + /// No hash, returns `X` component of shared point only. + /// + /// [ansix963]: https://webstore.ansi.org/standards/ascx9/ansix9632011r2017 + case ansiX963 + + /// Following no standard at all. + /// + /// No hash, returns the whole shared point. + /// + case noHashWholePoint + + func hashfp() -> (Optional<@convention(c) (Optional>, Optional>, Optional>, Optional) -> Int32>) { + switch self { + case .libsecp256kDefault: return secp256k1_ecdh_hash_function_default + case .ansiX963: return ecdh_asn1_x963 + case .noHashWholePoint: return ecdh_unsafe_whole_point + } + } + + var outputByteCount: Int { + switch self { + case .libsecp256kDefault: return Curve.Field.byteCount + case .ansiX963: return Curve.Field.byteCount + case .noHashWholePoint: return K1.Format.uncompressed.length + } + } + } +} + +// MARK: ECDH +extension FFI.ECDH { + static func keyExchange( + publicKey: FFI.PublicKey.Wrapped, + privateKey: FFI.PrivateKey.Wrapped, + serializeOutputFunction hashFp: SerializeFunction + ) throws -> Data { + + var sharedPublicPointBytes = [UInt8]( + repeating: 0, + count: hashFp.outputByteCount + ) + var arbitraryData: [UInt8]? = { + switch hashFp { + case let .libsecp256kDefault(arbitraryData?): return [UInt8](arbitraryData) + case .libsecp256kDefault(.none): return nil + case .ansiX963, .noHashWholePoint: return nil + } + }() + var publicKeyRaw = publicKey.raw + try FFI.call( + ifFailThrow: .failedToPerformDiffieHellmanKeyExchange + ) { context in + secp256k1_ecdh( + context, + &sharedPublicPointBytes, // output + &publicKeyRaw, // pubkey + privateKey.secureBytes.backing.bytes, // seckey + hashFp.hashfp(), // hashfp + &arbitraryData // arbitrary data pointer that is passed through to hashfp + ) + } + + return Data(sharedPublicPointBytes) + + } +} + + diff --git a/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift b/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift new file mode 100644 index 0000000..8324f86 --- /dev/null +++ b/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift @@ -0,0 +1,102 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-19. +// + +import Foundation +import secp256k1 + +extension FFI { + public enum ECDSA {} +} +extension FFI.ECDSA { + public enum Recovery {} + public enum NonRecovery {} +} + + +protocol RawECDSASignature { + init() +} +extension secp256k1_ecdsa_recoverable_signature: RawECDSASignature {} +extension secp256k1_ecdsa_signature: RawECDSASignature {} + +protocol WrappedECDSASignature { + associatedtype Raw: RawECDSASignature + init(raw: Raw) + var raw: Raw { get } + static func sign() -> (OpaquePointer, UnsafeMutablePointer, UnsafePointer, UnsafePointer, secp256k1_nonce_function?, UnsafeRawPointer?) -> Int32 +} + +// MARK: ECDSA Shared +extension FFI.ECDSA { + + internal static func _sign( + message: [UInt8], + privateKey: FFI.PrivateKey.Wrapped, + options: K1.ECDSA.SigningOptions = .default + ) throws -> WrappedSignature where WrappedSignature: WrappedECDSASignature { + guard message.count == Curve.Field.byteCount else { + throw K1.Error.unableToSignMessageHasInvalidLength( + got: message.count, + expected: Curve.Field.byteCount + ) + } + + var raw = WrappedSignature.Raw() + + try FFI.call( + ifFailThrow: .failedToECDSASignDigest + ) { context in + WrappedSignature.sign()( + context, + &raw, + message, + privateKey.secureBytes.backing.bytes, + options.nonceFunction.function(), + options.arbitraryData + ) + } + + return .init(raw: raw) + + } +} +extension K1.ECDSA.SigningOptions { + fileprivate var arbitraryData: [UInt8]? { + switch self.nonceFunction { + case .random: return nil + case let .deterministic(arbitraryData): return arbitraryData?.arbitraryData + } + } + +} +extension K1.ECDSA.SigningOptions.NonceFunction { + fileprivate func function() -> Optional<@convention(c) (Optional>, Optional>, Optional>, Optional>, Optional, UInt32) -> Int32> { + switch self { + case .deterministic: + return secp256k1_nonce_function_rfc6979 + + case .random: + return { ( + nonce32: UnsafeMutablePointer?, // Out: pointer to a 32-byte array to be filled by the function. + msg: UnsafePointer?, // In: the 32-byte message hash being verified (will not be NULL) + key32: UnsafePointer?, // In: pointer to a 32-byte secret key (will not be NULL) + algo16: UnsafePointer?, // In: pointer to a 16-byte array describing the signature algorithm (will be NULL for ECDSA for compatibility). + arbitraryData: UnsafeMutableRawPointer?, // In: Arbitrary data pointer that is passed through. + attemptIndex: UInt32 // In: how many iterations we have tried to find a nonce. This will almost always be 0, but different attempt values are required to result in a different nonce. + ) -> Int32 /* Returns: 1 if a nonce was successfully generated. 0 will cause signing to fail. */ in + + SecureBytes(count: Curve.Field.byteCount).withUnsafeBytes { + nonce32?.assign(from: $0.baseAddress!.assumingMemoryBound(to: UInt8.self), count: $0.count) + } + + // Returns: 1 if a nonce was successfully generated. 0 will cause signing to fail. + return 1 + } + } + } +} + diff --git a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift new file mode 100644 index 0000000..6cbd0a6 --- /dev/null +++ b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift @@ -0,0 +1,34 @@ +import Foundation +import secp256k1 + +// MARK: Wrapped +extension FFI.ECDSA.NonRecovery { + + struct Wrapped: @unchecked Sendable, ContiguousBytes, WrappedECDSASignature { + typealias Raw = secp256k1_ecdsa_signature + let raw: Raw + init(raw: Raw) { + self.raw = raw + } + } +} + + +// MARK: Sign +extension FFI.ECDSA.NonRecovery.Wrapped { + static func sign() -> (OpaquePointer, UnsafeMutablePointer, UnsafePointer, UnsafePointer, secp256k1_nonce_function?, UnsafeRawPointer?) -> Int32 { + secp256k1_ecdsa_sign + } +} + +// MARK: ContiguousBytes +extension FFI.ECDSA.NonRecovery.Wrapped { + func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + var rawData = raw.data + return try Swift.withUnsafeBytes(of: &rawData) { pointer in + try body(pointer) + } + } +} + + diff --git a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift new file mode 100644 index 0000000..1936376 --- /dev/null +++ b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift @@ -0,0 +1,174 @@ +import Foundation +import secp256k1 + +// MARK: Deserialize +extension FFI.ECDSA.NonRecovery { + static let byteCount = 2 * Curve.Field.byteCount + + /// Compact aka `IEEE P1363` aka `R||S`. + static func from( + compactBytes: [UInt8] + ) throws -> Wrapped { + try Wrapped( + raw: Raw.nonRecoverableSignature(compactBytes: compactBytes) + ) + } + + static func from( + derRepresentation: [UInt8] + ) throws -> Wrapped { + try Wrapped( + raw: Raw.nonRecoverableSignature(derBytes: derRepresentation) + ) + } +} + +// MARK: Serialize +extension FFI.ECDSA.NonRecovery { + static func compact(_ wrapped: Wrapped) throws -> Data { + var out = [UInt8](repeating: 0, count: Self.byteCount) + var rawSignature = wrapped.raw + try FFI.call(ifFailThrow: .failedToSerializeSignature) { context in + secp256k1_ecdsa_signature_serialize_compact(context, &out, &rawSignature) + } + return Data(out) + } + + static func der( + _ wrapped: Wrapped + ) throws -> Data { + var derMaxLength = 75 // in fact max is 73, but we can have some margin. + var derSignature = [UInt8](repeating: 0, count: derMaxLength) + var rawSignature = wrapped.raw + try FFI.call(ifFailThrow: .failedToSerializeDERSignature) { context in + secp256k1_ecdsa_signature_serialize_der( + context, + &derSignature, + &derMaxLength, + &rawSignature + ) + } + return Data(derSignature.prefix(derMaxLength)) + } +} + +// MARK: Recover +extension FFI.ECDSA.NonRecovery { + + static func recoverPublicKey( + _ wrapped: Wrapped, + recoveryID: Int32, + message: [UInt8] + ) throws -> FFI.PublicKey.Wrapped { + guard message.count == Curve.Field.byteCount else { + throw K1.Error.unableToRecoverMessageHasInvalidLength(got: message.count, expected: Curve.Field.byteCount) + } + let nonRecoverableCompact = try FFI.ECDSA.NonRecovery.compact(wrapped) + return try Self.recoverPublicKey( + nonRecoverableCompact: nonRecoverableCompact, + recoveryID: recoveryID, + message: message + ) + } + + static func recoverPublicKey( + nonRecoverableCompact: Data, + recoveryID: Int32, + message: [UInt8] + ) throws -> FFI.PublicKey.Wrapped { + guard message.count == Curve.Field.byteCount else { + throw K1.Error.unableToRecoverMessageHasInvalidLength( + got: message.count, + expected: Curve.Field.byteCount + ) + } + var compact = [UInt8](nonRecoverableCompact) + var recoverable = secp256k1_ecdsa_recoverable_signature() + try FFI.call(ifFailThrow: .failedToParseRecoverableSignatureFromCompact) { context in + secp256k1_ecdsa_recoverable_signature_parse_compact( + context, + &recoverable, + &compact, + recoveryID + ) + } + var publicKeyRaw = secp256k1_pubkey() + try FFI.call(ifFailThrow: .failedToRecoverPublicKey) { context in + secp256k1_ecdsa_recover( + context, + &publicKeyRaw, + &recoverable, + message + ) + } + return FFI.PublicKey.Wrapped(raw: publicKeyRaw) + } +} + + +// MARK: Validate +extension FFI.ECDSA.NonRecovery { + static func isValid( + ecdsaSignature: FFI.ECDSA.NonRecovery.Wrapped, + publicKey: FFI.PublicKey.Wrapped, + message: [UInt8], + options: K1.ECDSA.ValidationOptions = .default + ) throws -> Bool { + try FFI.toC { ffi -> Bool in + var publicKeyRaw = publicKey.raw + var maybeMalleable = ecdsaSignature.raw + var normalized = secp256k1_ecdsa_signature() + + let codeForSignatureWasMalleable = 1 + let signatureWasMalleableResult = ffi.callWithResultCode { context in + secp256k1_ecdsa_signature_normalize(context, &normalized, &maybeMalleable) + } + let signatureWasMalleable = signatureWasMalleableResult == codeForSignatureWasMalleable + let isSignatureValid = ffi.validate { context in + secp256k1_ecdsa_verify( + context, + &normalized, + message, + &publicKeyRaw + ) + } + let acceptMalleableSignatures = options.malleabilityStrictness == .accepted + switch (isSignatureValid, signatureWasMalleable, acceptMalleableSignatures) { + case (true, false, _): + // Signature is valid + return true + case (true, true, true): + // Signature was valid but malleable, since you specified to + // accept malleability => considering signature valid. + return true + case (true, true, false): + // Signature was valid, but not normalized which was required => + // considering signature invalid. + return false + case (false, _, _): + // Signature is invalid. + return false + } + } + } + +} + + +// MARK: Sign +extension FFI.ECDSA.NonRecovery { + + /// Produces a **non recoverable** ECDSA signature from a hashed `message` + static func sign( + hashedMessage: [UInt8], + privateKey: FFI.PrivateKey.Wrapped, + options: K1.ECDSA.SigningOptions = .default + ) throws -> FFI.ECDSA.NonRecovery.Wrapped { + + try FFI.ECDSA._sign( + message: hashedMessage, + privateKey: privateKey, + options: options + ) + } +} diff --git a/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift b/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift new file mode 100644 index 0000000..40cbe06 --- /dev/null +++ b/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift @@ -0,0 +1,33 @@ +import Foundation +import secp256k1 + + +// MARK: ECDSA Recovery Wrapped +extension FFI.ECDSA.Recovery { + struct Wrapped: @unchecked Sendable, ContiguousBytes, WrappedECDSASignature { + typealias Raw = secp256k1_ecdsa_recoverable_signature + let raw: Raw + init(raw: Raw) { + self.raw = raw + } + } +} + +// MARK: Sign +extension FFI.ECDSA.Recovery.Wrapped { + + static func sign() -> (OpaquePointer, UnsafeMutablePointer, UnsafePointer, UnsafePointer, secp256k1_nonce_function?, UnsafeRawPointer?) -> Int32 { + secp256k1_ecdsa_sign_recoverable + } +} + +// MARK: ContiguousBytes +extension FFI.ECDSA.Recovery.Wrapped { + + func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + var rawData = raw.data + return try Swift.withUnsafeBytes(of: &rawData) { pointer in + try body(pointer) + } + } +} diff --git a/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift b/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift new file mode 100644 index 0000000..63c7094 --- /dev/null +++ b/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift @@ -0,0 +1,122 @@ +import Foundation +import secp256k1 + +// MARK: Deserialize +extension FFI.ECDSA.Recovery { + static let byteCount = FFI.ECDSA.NonRecovery.byteCount + 1 + + static func deserialize( + rawRepresentation: some DataProtocol + ) throws -> Wrapped { + try Wrapped(raw: Raw.recoverableSignature(rawRepresentation)) + } + + /// Compact aka `IEEE P1363` aka `R||S`. + static func deserialize( + compact rs: [UInt8], + recoveryID recid: Int32 + ) throws -> Wrapped { + var raw = Wrapped.Raw() + try FFI.call( + ifFailThrow: .failedSignatureToConvertRecoverableSignatureToCompact + ) { context in + secp256k1_ecdsa_recoverable_signature_parse_compact( + context, + &raw, + rs, + recid + ) + } + return .init(raw: raw) + } +} + + +// MARK: Serialize +extension FFI.ECDSA.Recovery { + static func serializeCompact( + _ wrapped: Wrapped + ) throws -> (rs: [UInt8], recoveryID: Int32) { + var rs = [UInt8](repeating: 0, count: FFI.ECDSA.NonRecovery.byteCount) + var recoveryID: Int32 = 0 + var rawSignature = wrapped.raw + try FFI.call( + ifFailThrow: .failedSignatureToConvertRecoverableSignatureToCompact + ) { context in + secp256k1_ecdsa_recoverable_signature_serialize_compact( + context, + &rs, + &recoveryID, + &rawSignature + ) + } + return (rs, recoveryID) + } +} + +// MARK: Convert +extension FFI.ECDSA.Recovery { + static func nonRecoverable( + _ wrapped: Wrapped + ) throws -> FFI.ECDSA.NonRecovery.Wrapped { + + + var nonRecoverable = secp256k1_ecdsa_signature() + var recoverable = wrapped.raw + + try FFI.call( + ifFailThrow: .failedToConvertRecoverableSignatureToNonRecoverable + ) { context in + secp256k1_ecdsa_recoverable_signature_convert( + context, + &nonRecoverable, + &recoverable + ) + } + + return .init(raw: nonRecoverable) + } +} + +// MARK: Recover +extension FFI.ECDSA.Recovery { + static func recover( + _ wrapped: Wrapped, + message: [UInt8] + ) throws -> FFI.PublicKey.Wrapped { + guard message.count == Curve.Field.byteCount else { + throw K1.Error.unableToRecoverMessageHasInvalidLength(got: message.count, expected: Curve.Field.byteCount) + } + var rawSignature = wrapped.raw + var rawPublicKey = secp256k1_pubkey() + try FFI.call( + ifFailThrow: .failedToRecoverPublicKey + ) { context in + secp256k1_ecdsa_recover( + context, + &rawPublicKey, + &rawSignature, + message + ) + } + return FFI.PublicKey.Wrapped(raw: rawPublicKey) + } +} + +// MARK: Sign +extension FFI.ECDSA.Recovery { + + /// Produces a **recoverable** ECDSA signature from a hashed `message` + static func sign( + hashedMessage: [UInt8], + privateKey: K1.PrivateKeyImpl.Wrapped, + options: K1.ECDSA.SigningOptions = .default + ) throws -> FFI.ECDSA.Recovery.Wrapped { + + try FFI.ECDSA._sign( + message: hashedMessage, + privateKey: privateKey, + options: options + ) + } +} diff --git a/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift b/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift new file mode 100644 index 0000000..192073a --- /dev/null +++ b/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift @@ -0,0 +1,91 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-18. +// + +import Foundation +import secp256k1 + +extension FFI { + enum PrivateKey { + struct Wrapped: @unchecked Sendable { + + let publicKey: FFI.PublicKey.Wrapped + internal let secureBytes: SecureBytes + + fileprivate init(secureBytes: SecureBytes) throws { + guard secureBytes.count == Curve.Field.byteCount else { + throw K1.Error.failedToInitializePrivateKeyIncorrectByteCount( + got: secureBytes.count, + expected: Curve.Field.byteCount + ) + } + + if secureBytes.allSatisfy({ $0 == .zero }) { + throw K1.Error.invalidPrivateKeyMustNotBeZero + } + + self.secureBytes = secureBytes + var secureBytes = secureBytes + self.publicKey = try secureBytes.withUnsafeMutableBytes { seckey in + var raw = secp256k1_pubkey() + + try FFI.call(ifFailThrow: .invalidPrivateKeyMustBeSmallerThanOrder) { context in + secp256k1_ec_pubkey_create(context, &raw, seckey.baseAddress!) + } + + return FFI.PublicKey.Wrapped(raw: raw) + } + } + } + } +} + +// MARK: Init +extension FFI.PrivateKey.Wrapped { + + fileprivate init(bytes: [UInt8]) throws { + try self.init(secureBytes: .init(bytes: bytes)) + } + + init() { + func generateNew() -> SecureBytes { + var attempt = 0 + + while attempt < 100 { + defer { attempt += 1 } + do { + let secureBytes = SecureBytes(count: Curve.Field.byteCount) + let _ = try FFI.PrivateKey.Wrapped(secureBytes: secureBytes) + return secureBytes + } catch { + // Failure (due to unlikely scenario that the private key scalar > order of the curve) => retry + } + } + + // Probability of this happening is: + // n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + // (n / 2^256) ^ 100 = lim 0 + // I.e. will not happen. + fatalError( + """ + Failed to generate private key after #\(attempt) attempts. + You are the most unlucky person in the universe. + Or by Occam's razor: the person writing this code made some error. + """ + ) + } + try! self.init(secureBytes: generateNew()) + } +} + +// MARK: Deserialize +extension FFI.PrivateKey { + static func deserialize( + rawRepresentation: some ContiguousBytes + ) throws -> Wrapped { + try Wrapped(bytes: rawRepresentation.bytes) + } +} diff --git a/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift b/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift new file mode 100644 index 0000000..2ff6230 --- /dev/null +++ b/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift @@ -0,0 +1,109 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-20. +// + +import Foundation +import secp256k1 + +// MARK: Deserialize +extension FFI.PublicKey { + + /// `04 || X || Y` (65 bytes) + public static let x963ByteCount = 1 + (2 * Curve.Field.byteCount) + + /// `X || Y` (64 bytes) + public static let rawByteCount = 2 * Curve.Field.byteCount + + + /// `02|03 || X` (33 bytes) + public static let compressedByteCount = 1 + Curve.Field.byteCount + + /// `04 || X || Y` (65 bytes) + static func deserialize( + x963Representation contiguousBytes: some ContiguousBytes + ) throws -> Wrapped { + try contiguousBytes.withUnsafeBytes { bufferPointer throws -> Wrapped in + let expected = Self.x963ByteCount + guard bufferPointer.count == expected else { + throw K1.Error.incorrectByteCountOfX963PublicKey(got: bufferPointer.count, expected: expected) + } + return try Self._deserialize(bytes: [UInt8](bufferPointer)) + } + } + + /// `X || Y` (64 bytes) + static func deserialize( + rawRepresentation contiguousBytes: some ContiguousBytes + ) throws -> Wrapped { + try contiguousBytes.withUnsafeBytes { bufferPointer throws -> Wrapped in + let expected = Self.rawByteCount + guard bufferPointer.count == expected else { + throw K1.Error.incorrectByteCountOfRawPublicKey(got: bufferPointer.count, expected: expected) + } + // We can simply prepend `04` and parse as x963Representation + do { + return try Self.deserialize(x963Representation: [0x04] + [UInt8](bufferPointer)) + } catch { + throw K1.Error.unableToDeserializePublicKeyFromRawRepresentation + } + } + } + + /// `02|03 || X` (33 bytes) + static func deserialize( + compressedRepresentation contiguousBytes: some ContiguousBytes + ) throws -> Wrapped { + + try contiguousBytes.withUnsafeBytes { bufferPointer throws -> Wrapped in + let expected = Self.compressedByteCount + guard bufferPointer.count == expected else { + throw K1.Error.incorrectByteCountOfCompressedPublicKey(got: bufferPointer.count, expected: expected) + } + return try Self._deserialize(bytes: [UInt8](bufferPointer)) + } + + } + + private static func _deserialize(bytes: [UInt8]) throws -> Wrapped { + var raw = secp256k1_pubkey() + try FFI.call( + ifFailThrow: .failedToDeserializePublicKey + ) { context in + secp256k1_ec_pubkey_parse( + context, + &raw, + bytes, + bytes.count + ) + + } + return .init(raw: raw) + } +} + +// MARK: Serialize +extension FFI.PublicKey { + + static func serialize( + _ wrapped: Wrapped, + format: K1.Format + ) throws -> Data { + var byteCount = format.length + var out = [UInt8](repeating: 0x00, count: byteCount) + var publicKeyRaw = wrapped.raw + try FFI.call(ifFailThrow: .failedToSerializePublicKey) { context in + secp256k1_ec_pubkey_serialize( + context, + &out, + &byteCount, + &publicKeyRaw, + format.rawValue + ) + } + return Data(out.prefix(byteCount)) + } + +} diff --git a/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift b/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift new file mode 100644 index 0000000..cb90a45 --- /dev/null +++ b/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift @@ -0,0 +1,50 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-19. +// + +import Foundation +import secp256k1 + +extension FFI { + enum PublicKey {} +} + +// MARK: PublicKey Wrapped +extension FFI.PublicKey { + + struct Wrapped: @unchecked Sendable, ContiguousBytes { + typealias Raw = secp256k1_pubkey + let raw: Raw + init(raw: Raw) { + self.raw = raw + } + } +} + +// MARK: ContiguousBytes +extension FFI.PublicKey.Wrapped { + func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + var rawData = raw.data + return try Swift.withUnsafeBytes(of: &rawData) { pointer in + try body(pointer) + } + } +} + +// MARK: Comparison +extension FFI.PublicKey.Wrapped { + func compare(to other: FFI.PublicKey.Wrapped) throws -> Bool { + var selfRaw = self.raw + var otherRaw = other.raw + return try FFI.toC { ffi in + ffi.callWithResultCode { context in + secp256k1_ec_pubkey_cmp(context, &selfRaw, &otherRaw) + } == 0 + } + + } +} + diff --git a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift new file mode 100644 index 0000000..9f4fcd4 --- /dev/null +++ b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift @@ -0,0 +1,97 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-19. +// + +import Foundation +import secp256k1 + +extension FFI { + enum Schnorr {} +} + +// MARK: Schnorr Validate +extension FFI.Schnorr { + + static func isValid( + signature: FFI.Schnorr.Wrapped, + publicKey: FFI.PublicKey.Wrapped, + message: [UInt8] + ) throws -> Bool { + + try FFI.toC { ffi -> Bool in + var publicKeyX = secp256k1_xonly_pubkey() + var publicKeyRaw = publicKey.raw + try FFI.call(ifFailThrow: .failedToSchnorrVerifyGettingXFromPubKey) { context in + secp256k1_xonly_pubkey_from_pubkey( + context, + &publicKeyX, + nil, + &publicKeyRaw + ) + } + + return ffi.validate { context in + secp256k1_schnorrsig_verify( + context, + signature.bytes, + message, + message.count, + &publicKeyX + ) + } + } + } +} + +// MARK: Schnorr Sign +extension FFI.Schnorr { + + static func sign( + hashedMessage message: [UInt8], + privateKey: FFI.PrivateKey.Wrapped, + options: K1.Schnorr.SigningOptions = .default + ) throws -> FFI.Schnorr.Wrapped { + + guard + message.count == Curve.Field.byteCount + else { + throw K1.Error.unableToSignMessageHasInvalidLength( + got: message.count, + expected: Curve.Field.byteCount + ) + } + + var signatureOut = [UInt8](repeating: 0, count: FFI.Schnorr.Wrapped.byteCount) + + var keyPair = secp256k1_keypair() + + try FFI.call( + ifFailThrow: .failedToInitializeKeyPairForSchnorrSigning + ) { context in + secp256k1_keypair_create( + context, + &keyPair, + privateKey.secureBytes.backing.bytes + ) + } + + + try FFI.call( + ifFailThrow: .failedToSchnorrSignDigest + ) { context in + secp256k1_schnorrsig_sign32( + context, + &signatureOut, + message, + &keyPair, + options.auxilaryRandomData?.aux + ) + } + + return try FFI.Schnorr.Wrapped(bytes: signatureOut) + } + +} diff --git a/Sources/K1/Support/FFI/API/Schnorr/Schnorr+Wrapped.swift b/Sources/K1/Support/FFI/API/Schnorr/Schnorr+Wrapped.swift new file mode 100644 index 0000000..0967bed --- /dev/null +++ b/Sources/K1/Support/FFI/API/Schnorr/Schnorr+Wrapped.swift @@ -0,0 +1,47 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-20. +// + +import Foundation + +extension FFI.Schnorr { + struct Wrapped: @unchecked Sendable, Hashable { + static let byteCount = 2 * Curve.Field.byteCount + + internal let bytes: [UInt8] + + init(bytes: [UInt8]) throws { + guard bytes.count == Self.byteCount else { + throw K1.Error.failedToInitSchnorrSignatureInvalidByteCount( + got: bytes.count, + expected: Self.byteCount + ) + } + self.bytes = bytes + } + } +} + +// MARK: Serialization +extension FFI.Schnorr.Wrapped { + var rawRepresentation: Data { + Data(bytes) + } +} + +// MARK: Equatable +extension FFI.Schnorr.Wrapped { + static func == (lhs: FFI.Schnorr.Wrapped, rhs: FFI.Schnorr.Wrapped) -> Bool { + lhs.bytes == rhs.bytes + } +} + +// MARK: Hashable +extension FFI.Schnorr.Wrapped { + func hash(into hasher: inout Hasher) { + hasher.combine(bytes) + } +} diff --git a/Sources/K1/Bridge/BridgeToC.swift b/Sources/K1/Support/FFI/Internals/FFI+Call.swift similarity index 78% rename from Sources/K1/Bridge/BridgeToC.swift rename to Sources/K1/Support/FFI/Internals/FFI+Call.swift index 0a28e6a..44ef514 100644 --- a/Sources/K1/Bridge/BridgeToC.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Call.swift @@ -9,13 +9,13 @@ import CryptoKit import secp256k1 -internal final class Bridge { +final class FFI { let context: OpaquePointer init() throws { guard /* "Create a secp256k1 context object." */ - let context = secp256k1_context_create(K1.Context.sign.rawValue | K1.Context.verify.rawValue) + let context = secp256k1_context_create(Context.sign.rawValue | Context.verify.rawValue) else { throw K1.Error.failedToCreateContextForSecp256k1 } @@ -29,13 +29,13 @@ internal final class Bridge { } } -extension Bridge { +extension FFI { static func toC( - _ closure: (Bridge) throws -> T + _ closure: (FFI) throws -> T ) throws -> T { - let bridge = try Bridge() - return try closure(bridge) + let ffi = try FFI() + return try closure(ffi) } /// Returns `true` iff result code is `1` @@ -67,8 +67,8 @@ extension Bridge { ifFailThrow error: K1.Error, _ method: (OpaquePointer) -> Int32 ) throws { - try toC { bridge in - try bridge.call(ifFailThrow: error, method) + try toC { ffi in + try ffi.call(ifFailThrow: error, method) } } } diff --git a/Sources/K1/K1/K1/K1+Bridge+To+C.swift b/Sources/K1/Support/FFI/Internals/FFI+Context.swift similarity index 55% rename from Sources/K1/K1/K1/K1+Bridge+To+C.swift rename to Sources/K1/Support/FFI/Internals/FFI+Context.swift index d91312e..980c9bf 100644 --- a/Sources/K1/K1/K1/K1+Bridge+To+C.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Context.swift @@ -5,12 +5,24 @@ // Created by Alexander Cyon on 2022-01-27. // - -// Bridge to C +// FFI to C import secp256k1 +extension FFI { + + /// Bridging type used for underlying libsecp256k1 methods: + /// - `secp256k1_context_create` + /// - `secp256k1_context_preallocated_size` + /// - `secp256k1_context_preallocated_create` + enum Context: UInt32 { + case none, sign, verify + } + +} + + // MARK: Context -extension K1.Context { +extension FFI.Context { /// Bridging value used by libsecp256k1 methods that requires info about /// how the context is used, e.g. for signing or verification (validate). @@ -26,19 +38,3 @@ extension K1.Context { return UInt32(value) } } - -// MARK: Format -public extension K1.Format { - - /// Bridging value used by libsecp256k1 public key specifying the format - /// of the imported public key, i.e. how many bytes. - var rawValue: UInt32 { - let value: Int32 - switch self { - case .compressed: value = SECP256K1_EC_COMPRESSED - case .uncompressed: value = SECP256K1_EC_UNCOMPRESSED - } - - return UInt32(value) - } -} diff --git a/Sources/K1/Support/FFI/Internals/FFI+Format.swift b/Sources/K1/Support/FFI/Internals/FFI+Format.swift new file mode 100644 index 0000000..7634cd5 --- /dev/null +++ b/Sources/K1/Support/FFI/Internals/FFI+Format.swift @@ -0,0 +1,53 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-17. +// + +import Foundation +import secp256k1 + +// MARK: Format +extension K1.Format { + + /// Bridging value used by libsecp256k1 key specifying the format + /// of the imported key, i.e. how many bytes. + public var rawValue: UInt32 { + let value: Int32 + switch self { + case .compressed: value = SECP256K1_EC_COMPRESSED + case .uncompressed: value = SECP256K1_EC_UNCOMPRESSED + } + + return UInt32(value) + } +} + +extension K1 { + + // Bridging type for: `secp256k1_ec_pubkey_serialize` + public enum Format: UInt32, CaseIterable { + case compressed, uncompressed + } +} + +extension K1.Format { + + var length: Int { + switch self { + case .compressed: return 33 + case .uncompressed: return 65 + } + } + + internal init(byteCount: Int) throws { + if byteCount == Self.uncompressed.length { + self = .uncompressed + } else if byteCount == Self.compressed.length { + self = .compressed + } else { + fatalError("invalid byte count: \(byteCount)") + } + } +} diff --git a/Sources/K1/Support/FFI/Internals/FFI+Raw.swift b/Sources/K1/Support/FFI/Internals/FFI+Raw.swift new file mode 100644 index 0000000..d329378 --- /dev/null +++ b/Sources/K1/Support/FFI/Internals/FFI+Raw.swift @@ -0,0 +1,70 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-17. +// + +import Foundation +import secp256k1 + +enum Raw {} + +extension Raw { + + static func recoverableSignature( + _ rawRepresentation: some DataProtocol + ) throws -> secp256k1_ecdsa_recoverable_signature { + let expected = K1.ECDSA.Recoverable.Signature.Compact.byteCount + guard + rawRepresentation.count == expected + else { + throw K1.Error.incorrectByteCountOfRawRecoverableSignature( + got: rawRepresentation.count, + expected: expected + ) + } + var raw = secp256k1_ecdsa_recoverable_signature() + withUnsafeMutableBytes(of: &raw.data) { pointer in + pointer.copyBytes( + from: rawRepresentation.prefix(pointer.count) + ) + } + return raw + } + + static func nonRecoverableSignature( + compactBytes: [UInt8] + ) throws -> secp256k1_ecdsa_signature { + + var raw = secp256k1_ecdsa_signature() + + try FFI.call(ifFailThrow: .failedToParseNonRecoverableSignatureFromCompactRepresentation) { context in + secp256k1_ecdsa_signature_parse_compact( + context, + &raw, + compactBytes + ) + } + + return raw + } + + static func nonRecoverableSignature( + derBytes: [UInt8] + ) throws -> secp256k1_ecdsa_signature { + + var raw = secp256k1_ecdsa_signature() + + try FFI.call(ifFailThrow: .failedToParseNonRecoverableSignatureFromCompactRepresentation) { context in + secp256k1_ecdsa_signature_parse_der( + context, + &raw, + derBytes, + derBytes.count + ) + } + + return raw + } +} diff --git a/Sources/K1/Support/Misc/Curve.swift b/Sources/K1/Support/Misc/Curve.swift new file mode 100644 index 0000000..ac2bac9 --- /dev/null +++ b/Sources/K1/Support/Misc/Curve.swift @@ -0,0 +1,24 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-19. +// + +import Foundation + +// MARK: - Curve + +/// Details about the elliptic curve `secp256k1`. +enum Curve {} + +// MARK: - FiniteField +extension Curve { + /// The finite field of the secp256k1 curve. + enum Field {} +} + +extension Curve.Field { + /// Finite field members are 256 bits large, i.e. 32 bytes. + static let byteCount = 32 +} diff --git a/Sources/K1/Support/Misc/K1+Error.swift b/Sources/K1/Support/Misc/K1+Error.swift new file mode 100644 index 0000000..d2ca959 --- /dev/null +++ b/Sources/K1/Support/Misc/K1+Error.swift @@ -0,0 +1,57 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-17. +// + +import Foundation + +extension K1 { + enum Error: Sendable, Swift.Error, Hashable { + case invalidPEMDocument + case invalidPrivateKeyMustNotBeZero + case invalidPrivateKeyMustBeSmallerThanOrder + case failedToProduceSharedSecret + case failedToComputePublicKeyFromPrivateKey + case failedToPerformDiffieHellmanKeyExchange + case failedToInitializePrivateKeyIncorrectByteCount(got: Int, expected: Int) + case failedToCreateContextForSecp256k1 + case failedToDeserializePublicKey + case failedToSerializePublicKey + case failedToSchnorrVerifyGettingXFromPubKey + case incorrectByteCountOfX963PrivateKey(got: Int, expected: Int) + + /// The public key encoded in the x963 privateKey representation does not match the derived public key from the private key. + case invalidPrivateX963RepresentationPublicKeyDiscrepancy + + case incorrectByteCountOfX963PublicKey(got: Int, expected: Int) + case incorrectByteCountOfRawPublicKey(got: Int, expected: Int) + case unableToDeserializePublicKeyFromRawRepresentation + case incorrectByteCountOfCompressedPublicKey(got: Int, expected: Int) + case failedSignatureToConvertRecoverableSignatureToCompact + case failedToConvertRecoverableSignatureToNonRecoverable + case failedToRecoverPublicKey + case unableToSignMessageHasInvalidLength(got: Int, expected: Int) + case failedToInitializeKeyPairForSchnorrSigning + case failedToSchnorrSignDigestProvidedRandomnessInvalidLength + case failedToSchnorrSignDigest + case failedToInitSchnorrSignatureInvalidByteCount(got: Int, expected: Int) + case failedToSchnorrSignMessageInvalidLength + case incorrectByteCountOfArbitraryDataForNonceFunction + case invalidByteCount + case failedToECDSASignDigest + case unableToRecoverMessageHasInvalidLength(got: Int, expected: Int) + case failedToComparePublicKeys + case failedToSerializeDERSignature + case failedToSerializeSignature + case failedToParseRecoverableSignatureFromCompact + case incorrectByteCountOfRawSignature + case incorrectByteCountOfRawRecoverableSignature(got: Int, expected: Int) + case failedToParseNonRecoverableSignatureFromCompactRepresentation + case failedToDeserializeCompactRecoverableSignatureInvalidByteCount(got: Int, expected: Int) + case failedToDeserializeCompactRSRecoverableSignatureInvalidByteCount(got: Int, expected: Int) + case invalidRecoveryID(got: Int) + } +} + diff --git a/Sources/K1/Support/Misc/SharedSecret.swift b/Sources/K1/Support/Misc/SharedSecret.swift new file mode 100644 index 0000000..30c0b1f --- /dev/null +++ b/Sources/K1/Support/Misc/SharedSecret.swift @@ -0,0 +1,34 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-17. +// + +import Foundation + +// MUST match https://github.com/apple/swift-crypto/blob/main/Sources/Crypto/Key%20Agreement/DH.swift#L34 +import struct CryptoKit.SharedSecret + +/// A Key Agreement Result +/// A SharedSecret has to go through a Key Derivation Function before being able to use by a symmetric key operation. +struct __SharedSecret { + var ss: SecureBytes + + internal init(ss: SecureBytes) { + self.ss = ss + } +} + + +extension CryptoKit.SharedSecret { + init(data: Data) throws { + let __sharedSecret = __SharedSecret(ss: .init(bytes: data)) + let sharedSecret = unsafeBitCast(__sharedSecret, to: SharedSecret.self) + guard sharedSecret.withUnsafeBytes({ Data($0).count == data.count }) else { + throw K1.Error.failedToProduceSharedSecret + } + + self = sharedSecret + } +} diff --git a/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/ASN1.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/ASN1.swift new file mode 100644 index 0000000..b14b324 --- /dev/null +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/ASN1.swift @@ -0,0 +1,728 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +#if (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API +@_exported import CryptoKit +#else +import Foundation + +// This module implements "just enough" ASN.1. Specifically, we implement exactly enough ASN.1 DER parsing to handle +// the following use-cases: +// +// 1. Being able to parse the SPKI format for EC public keys +// 2. Being able to parse the PKCS#8 format for EC private keys +// 3. Being able to parse the SEC 1 format for EC private keys (produced by `openssl ec`) +// +// Let's talk about the DER encoding of ASN.1. DER is fundamentally a TLV (type length value) encoding. Each element is +// made of up some bytes that identify its type, some bytes that identify the length, and then the contents. In the full +// scheme of ASN.1 we care about a lot of things about its structure, but for our case we only care about a few kinds of +// tag. To work out the tag we need, we can look at the X.509 representation of an EC key public key, from RFC 5480 (for case 1), as +// well as the SEC 1 format for private keys and the PKCS#8 format for private keys. +// +// ### RFC 5480 SPKI: +// +// SubjectPublicKeyInfo ::= SEQUENCE { +// algorithm AlgorithmIdentifier, +// subjectPublicKey BIT STRING +// } +// +// ### SEC 1 +// +// For private keys, SEC 1 uses: +// +// ECPrivateKey ::= SEQUENCE { +// version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), +// privateKey OCTET STRING, +// parameters [0] EXPLICIT ECDomainParameters OPTIONAL, +// publicKey [1] EXPLICIT BIT STRING OPTIONAL +// } +// +// ### PKCS#8 +// +// For PKCS#8 we need the following for the private key: +// +// PrivateKeyInfo ::= SEQUENCE { +// version Version, +// privateKeyAlgorithm PrivateKeyAlgorithmIdentifier, +// privateKey PrivateKey, +// attributes [0] IMPLICIT Attributes OPTIONAL } +// +// Version ::= INTEGER +// +// PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier +// +// PrivateKey ::= OCTET STRING +// +// Attributes ::= SET OF Attribute +// +// ### Common +// +// Several of the above use formats defined here: +// +// AlgorithmIdentifier ::= SEQUENCE { +// algorithm OBJECT IDENTIFIER, +// parameters ANY DEFINED BY algorithm OPTIONAL +// } +// +// ECParameters ::= CHOICE { +// namedCurve OBJECT IDENTIFIER +// -- implicitCurve NULL +// -- specifiedCurve SpecifiedECDomain +// } +// +// For us, we expect the ECParameters structure to be using the namedCurve representation only: as we support only the NIST curves with ASN.1 +// there is no reason for the curve to ever not be named. +// +// Conveniently, this requires only a few data types from us: SEQUENCE, BIT STRING, OCTET STRING, and OBJECT IDENTIFIER. All three are +// universal objects for ASN.1. Their relevant characteristics are: +// +// ┌───────────────────┬────────────┬────────────────────────────────────────────────┬────────────────┬───────────┐ +// │ Name │ Tag Number │ Primitive │ Encoding Class │ Tag Bytes │ +// ├───────────────────┼────────────┼────────────────────────────────────────────────┼────────────────┼───────────┤ +// │ SEQUENCE │ 0x10 │ N │ Universal │ 0x30 │ +// │ BIT STRING │ 0x03 │ Y (we don't support constructed bit strings) │ Universal │ 0x03 │ +// │ OBJECT IDENTIFIER │ 0x06 │ Y │ Universal │ 0x06 │ +// | OCTET STRING | 0x04 | Y (we don't support constructed octet strings) | Universal | 0x04 | +// | INTEGER | 0x02 | Y | Universal | 0x02 | +// └───────────────────┴────────────┴────────────────────────────────────────────────┴────────────────┴───────────┘ +// +// The subjectPublicKey is required to be in x9.62 format, either compressed or uncompressed, so we can pass it directly to the +// initializers for CryptoKit once we've done the extraction. +// +// This is the complete set of things we need to be able to parse. To make our lives easier we try to parse this set of things somewhat +// generally: that is, we don't hard-code special knowledge of these formats as part of the parsing process. Instead we have written a +// parser that can divide the world of ASN.1 into parseable chunks, and then we try to decode specific formats from those chunks. This +// allows us to extend things in the future without too much pain. +internal enum ASN1 { } + +// MARK: - Parser Node +extension ASN1 { + /// An `ASN1ParserNode` is a representation of a parsed ASN.1 TLV section. An `ASN1ParserNode` may be primitive, or may be composed of other `ASN1ParserNode`s. + /// In our representation, we keep track of this by storing a node "depth", which allows rapid forward and backward scans to hop over sections + /// we're uninterested in. + /// + /// This type is not exposed to users of the API: it is only used internally for implementation of the user-level API. + fileprivate struct ASN1ParserNode { + /// The identifier. + var identifier: ASN1Identifier + + /// The depth of this node. + var depth: Int + + /// The data bytes for this node, if it is primitive. + var dataBytes: ArraySlice? + } +} + +extension ASN1.ASN1ParserNode: Hashable { } + +extension ASN1.ASN1ParserNode: CustomStringConvertible { + var description: String { + return "ASN1.ASN1ParserNode(identifier: \(self.identifier), depth: \(self.depth), dataBytes: \(self.dataBytes?.count ?? 0))" + } +} + +// MARK: - Sequence, SequenceOf, and Set +extension ASN1 { + /// Parse the node as an ASN.1 sequence. + internal static func sequence(_ node: ASN1Node, identifier: ASN1.ASN1Identifier, _ builder: (inout ASN1.ASN1NodeCollection.Iterator) throws -> T) throws -> T { + guard node.identifier == identifier, case .constructed(let nodes) = node.content else { + throw CryptoKitASN1Error.unexpectedFieldType + } + + var iterator = nodes.makeIterator() + + let result = try builder(&iterator) + + guard iterator.next() == nil else { + throw CryptoKitASN1Error.invalidASN1Object + } + + return result + } + + internal static func sequence(of: T.Type = T.self, identifier: ASN1.ASN1Identifier, rootNode: ASN1Node) throws -> [T] { + guard rootNode.identifier == identifier, case .constructed(let nodes) = rootNode.content else { + throw CryptoKitASN1Error.unexpectedFieldType + } + + return try nodes.map { try T(asn1Encoded: $0) } + } + + internal static func sequence(of: T.Type = T.self, identifier: ASN1.ASN1Identifier, nodes: inout ASN1.ASN1NodeCollection.Iterator) throws -> [T] { + guard let node = nodes.next() else { + // Not present, throw. + throw CryptoKitASN1Error.invalidASN1Object + } + + return try sequence(of: T.self, identifier: identifier, rootNode: node) + } + + /// Parse the node as an ASN.1 set. + internal static func set(_ node: ASN1Node, identifier: ASN1.ASN1Identifier, _ builder: (inout ASN1.ASN1NodeCollection.Iterator) throws -> T) throws -> T { + // Shhhh these two are secretly the same with identifier. + return try sequence(node, identifier: identifier, builder) + } +} + +// MARK: - Optional explicitly tagged +extension ASN1 { + /// Parses an optional explicitly tagged element. Throws on a tag mismatch, returns nil if the element simply isn't there. + /// + /// Expects to be used with the `ASN1.sequence` helper function. + internal static func optionalExplicitlyTagged(_ nodes: inout ASN1.ASN1NodeCollection.Iterator, tagNumber: Int, tagClass: ASN1.ASN1Identifier.TagClass, _ builder: (ASN1Node) throws -> T) throws -> T? { + var localNodesCopy = nodes + guard let node = localNodesCopy.next() else { + // Node not present, return nil. + return nil + } + + let expectedNodeID = ASN1.ASN1Identifier(explicitTagWithNumber: tagNumber, tagClass: tagClass) + assert(expectedNodeID.constructed) + guard node.identifier == expectedNodeID else { + // Node is a mismatch, with the wrong tag. Our optional isn't present. + return nil + } + + // We have the right optional, so let's consume it. + nodes = localNodesCopy + + // We expect a single child. + guard case .constructed(let nodes) = node.content else { + // This error is an internal parser error: the tag above is always constructed. + preconditionFailure("Explicit tags are always constructed") + } + + var nodeIterator = nodes.makeIterator() + guard let child = nodeIterator.next(), nodeIterator.next() == nil else { + throw CryptoKitASN1Error.invalidASN1Object + } + + return try builder(child) + } +} + +// MARK: - DEFAULT +extension ASN1 { + /// Parses a value that is encoded with a DEFAULT. Such a value is optional, and if absent will + /// be replaced with its default. + /// + /// Expects to be used with the `ASN1.sequence` helper function. + internal static func decodeDefault(_ nodes: inout ASN1.ASN1NodeCollection.Iterator, identifier: ASN1.ASN1Identifier, defaultValue: T, _ builder: (ASN1Node) throws -> T) throws -> T { + // A weird trick here: we only want to consume the next node _if_ it has the right tag. To achieve that, + // we work on a copy. + var localNodesCopy = nodes + guard let node = localNodesCopy.next() else { + // Whoops, nothing here. + return defaultValue + } + + guard node.identifier == identifier else { + // Node is a mismatch, with the wrong identifier. Our optional isn't present. + return defaultValue + } + + // We have the right optional, so let's consume it. + nodes = localNodesCopy + let parsed = try builder(node) + + // DER forbids encoding DEFAULT values at their default state. + // We can lift this in BER. + guard parsed != defaultValue else { + throw CryptoKitASN1Error.invalidASN1Object + } + + return parsed + } + + internal static func decodeDefaultExplicitlyTagged(_ nodes: inout ASN1.ASN1NodeCollection.Iterator, tagNumber: Int, tagClass: ASN1.ASN1Identifier.TagClass, defaultValue: T, _ builder: (ASN1Node) throws -> T) throws -> T { + if let result = try optionalExplicitlyTagged(&nodes, tagNumber: tagNumber, tagClass: tagClass, builder) { + guard result != defaultValue else { + // DER forbids encoding DEFAULT values at their default state. + // We can lift this in BER. + throw CryptoKitASN1Error.invalidASN1Object + } + + return result + } else { + return defaultValue + } + } +} + +// MARK: - Parsing +extension ASN1 { + /// A parsed representation of ASN.1. + fileprivate struct ASN1ParseResult { + private static let maximumNodeDepth = 10 + + var nodes: ArraySlice + + private init(_ nodes: ArraySlice) { + self.nodes = nodes + } + + fileprivate static func parse(_ data: ArraySlice) throws -> ASN1ParseResult { + var data = data + var nodes = [ASN1ParserNode]() + nodes.reserveCapacity(16) + + try parseNode(from: &data, depth: 1, into: &nodes) + guard data.count == 0 else { + throw CryptoKitASN1Error.invalidASN1Object + } + return ASN1ParseResult(nodes[...]) + } + + /// Parses a single ASN.1 node from the data and appends it to the buffer. This may recursively + /// call itself when there are child nodes for constructed nodes. + private static func parseNode(from data: inout ArraySlice, depth: Int, into nodes: inout [ASN1ParserNode]) throws { + guard depth <= ASN1.ASN1ParseResult.maximumNodeDepth else { + // We defend ourselves against stack overflow by refusing to allocate more than 10 stack frames to + // the parsing. + throw CryptoKitASN1Error.invalidASN1Object + } + + guard let rawIdentifier = data.popFirst() else { + throw CryptoKitASN1Error.truncatedASN1Field + } + + let identifier = try ASN1Identifier(rawIdentifier: rawIdentifier) + guard let wideLength = try data.readASN1Length() else { + throw CryptoKitASN1Error.truncatedASN1Field + } + + // UInt is sometimes too large for us! + guard let length = Int(exactly: wideLength) else { + throw CryptoKitASN1Error.invalidASN1Object + } + + var subData = data.prefix(length) + data = data.dropFirst(length) + + guard subData.count == length else { + throw CryptoKitASN1Error.truncatedASN1Field + } + + if identifier.constructed { + nodes.append(ASN1ParserNode(identifier: identifier, depth: depth, dataBytes: nil)) + while subData.count > 0 { + try parseNode(from: &subData, depth: depth + 1, into: &nodes) + } + } else { + nodes.append(ASN1ParserNode(identifier: identifier, depth: depth, dataBytes: subData)) + } + } + } +} + +extension ASN1.ASN1ParseResult: Hashable { } + +extension ASN1 { + static func parse(_ data: [UInt8]) throws -> ASN1Node { + return try parse(data[...]) + } + + static func parse(_ data: ArraySlice) throws -> ASN1Node { + var result = try ASN1ParseResult.parse(data) + + // There will always be at least one node if the above didn't throw, so we can safely just removeFirst here. + let firstNode = result.nodes.removeFirst() + + let rootNode: ASN1Node + if firstNode.identifier.constructed { + // We need to feed it the next set of nodes. + let nodeCollection = result.nodes.prefix { $0.depth > firstNode.depth } + result.nodes = result.nodes.dropFirst(nodeCollection.count) + rootNode = ASN1.ASN1Node(identifier: firstNode.identifier, content: .constructed(.init(nodes: nodeCollection, depth: firstNode.depth))) + } else { + rootNode = ASN1.ASN1Node(identifier: firstNode.identifier, content: .primitive(firstNode.dataBytes!)) + } + + precondition(result.nodes.count == 0, "ASN1ParseResult unexpectedly allowed multiple root nodes") + + return rootNode + } +} + +// MARK: - ASN1NodeCollection +extension ASN1 { + /// Represents a collection of ASN.1 nodes contained in a constructed ASN.1 node. + /// + /// Constructed ASN.1 nodes are made up of multiple child nodes. This object represents the collection of those child nodes. + /// It allows us to lazily construct the child nodes, potentially skipping over them when we don't care about them. + internal struct ASN1NodeCollection { + private var nodes: ArraySlice + + private var depth: Int + + fileprivate init(nodes: ArraySlice, depth: Int) { + self.nodes = nodes + self.depth = depth + + precondition(self.nodes.allSatisfy({ $0.depth > depth })) + if let firstDepth = self.nodes.first?.depth { + precondition(firstDepth == depth + 1) + } + } + } +} + +extension ASN1.ASN1NodeCollection: Sequence { + struct Iterator: IteratorProtocol { + private var nodes: ArraySlice + private var depth: Int + + fileprivate init(nodes: ArraySlice, depth: Int) { + self.nodes = nodes + self.depth = depth + } + + mutating func next() -> ASN1.ASN1Node? { + guard let nextNode = self.nodes.popFirst() else { + return nil + } + + assert(nextNode.depth == self.depth + 1) + if nextNode.identifier.constructed { + // We need to feed it the next set of nodes. + let nodeCollection = self.nodes.prefix { $0.depth > nextNode.depth } + self.nodes = self.nodes.dropFirst(nodeCollection.count) + return ASN1.ASN1Node(identifier: nextNode.identifier, content: .constructed(.init(nodes: nodeCollection, depth: nextNode.depth))) + } else { + // There must be data bytes here, even if they're empty. + return ASN1.ASN1Node(identifier: nextNode.identifier, content: .primitive(nextNode.dataBytes!)) + } + } + } + + func makeIterator() -> Iterator { + return Iterator(nodes: self.nodes, depth: self.depth) + } +} + +// MARK: - ASN1Node +extension ASN1 { + /// An `ASN1Node` is a single entry in the ASN.1 representation of a data structure. + /// + /// Conceptually, an ASN.1 data structure is rooted in a single node, which may itself contain zero or more + /// other nodes. ASN.1 nodes are either "constructed", meaning they contain other nodes, or "primitive", meaning they + /// store a base data type of some kind. + /// + /// In this way, ASN.1 objects tend to form a "tree", where each object is represented by a single top-level constructed + /// node that contains other objects and primitives, eventually reaching the bottom which is made up of primitive objects. + internal struct ASN1Node { + internal var identifier: ASN1Identifier + + internal var content: Content + } +} + +// MARK: - ASN1Node.Content +extension ASN1.ASN1Node { + /// The content of a single ASN1Node. + enum Content { + case constructed(ASN1.ASN1NodeCollection) + case primitive(ArraySlice) + } +} + +// MARK: - Serializing +extension ASN1 { + struct Serializer { + var serializedBytes: [UInt8] + + init() { + // We allocate a 1kB array because that should cover us most of the time. + self.serializedBytes = [] + self.serializedBytes.reserveCapacity(1024) + } + + /// Appends a single, non-constructed node to the content. + mutating func appendPrimitiveNode(identifier: ASN1.ASN1Identifier, _ contentWriter: (inout [UInt8]) throws -> Void) rethrows { + assert(identifier.primitive) + try self._appendNode(identifier: identifier) { try contentWriter(&$0.serializedBytes) } + } + + mutating func appendConstructedNode(identifier: ASN1.ASN1Identifier, _ contentWriter: (inout Serializer) throws -> Void) rethrows { + assert(identifier.constructed) + try self._appendNode(identifier: identifier, contentWriter) + } + + mutating func serialize(_ node: T) throws { + try node.serialize(into: &self) + } + + mutating func serialize(_ node: T, explicitlyTaggedWithTagNumber tagNumber: Int, tagClass: ASN1.ASN1Identifier.TagClass) throws { + return try self.serialize(explicitlyTaggedWithTagNumber: tagNumber, tagClass: tagClass) { coder in + try coder.serialize(node) + } + } + + mutating func serialize(explicitlyTaggedWithTagNumber tagNumber: Int, tagClass: ASN1.ASN1Identifier.TagClass, _ block: (inout Serializer) throws -> Void) rethrows { + let identifier = ASN1Identifier(explicitTagWithNumber: tagNumber, tagClass: tagClass) + try self.appendConstructedNode(identifier: identifier) { coder in + try block(&coder) + } + } + + mutating func serializeSequenceOf(_ elements: Elements, identifier: ASN1.ASN1Identifier = .sequence) throws where Elements.Element: ASN1Serializable { + try self.appendConstructedNode(identifier: identifier) { coder in + for element in elements { + try coder.serialize(element) + } + } + } + + mutating func serialize(_ node: ASN1.ASN1Node) { + let identifier = node.identifier + self._appendNode(identifier: identifier) { coder in + switch node.content { + case .constructed(let nodes): + for node in nodes { + coder.serialize(node) + } + case .primitive(let baseData): + coder.serializedBytes.append(contentsOf: baseData) + } + } + } + + // This is the base logical function that all other append methods are built on. This one has most of the logic, and doesn't + // police what we expect to happen in the content writer. + private mutating func _appendNode(identifier: ASN1.ASN1Identifier, _ contentWriter: (inout Serializer) throws -> Void) rethrows { + // This is a tricky game to play. We want to write the identifier and the length, but we don't know what the + // length is here. To get around that, we _assume_ the length will be one byte, and let the writer write their content. + // If it turns out to have been longer, we recalculate how many bytes we need and shuffle them in the buffer, + // before updating the length. Most of the time we'll be right: occasionally we'll be wrong and have to shuffle. + self.serializedBytes.writeIdentifier(identifier) + + // Write a zero for the length. + self.serializedBytes.append(0) + + // Save the indices and write. + let originalEndIndex = self.serializedBytes.endIndex + let lengthIndex = self.serializedBytes.index(before: originalEndIndex) + + try contentWriter(&self) + + let contentLength = self.serializedBytes.distance(from: originalEndIndex, to: self.serializedBytes.endIndex) + let lengthBytesNeeded = contentLength.bytesNeededToEncode + if lengthBytesNeeded == 1 { + // We can just set this at the top, and we're done! + assert(contentLength <= 0x7F) + self.serializedBytes[lengthIndex] = UInt8(contentLength) + return + } + + // Whoops, we need more than one byte to represent the length. That's annoying! + // To sort this out we want to "move" the memory to the right. + self.serializedBytes.moveRange(offset: lengthBytesNeeded - 1, range: originalEndIndex..> (shift * 8))) + } + + assert(writeIndex == self.serializedBytes.index(lengthIndex, offsetBy: lengthBytesNeeded - 1)) + } + } +} + +// MARK: - Helpers +internal protocol ASN1Parseable { + init(asn1Encoded: ASN1.ASN1Node) throws +} + +extension ASN1Parseable { + internal init(asn1Encoded sequenceNodeIterator: inout ASN1.ASN1NodeCollection.Iterator) throws { + guard let node = sequenceNodeIterator.next() else { + throw CryptoKitASN1Error.invalidASN1Object + } + + self = try .init(asn1Encoded: node) + } + + internal init(asn1Encoded: [UInt8]) throws { + self = try .init(asn1Encoded: ASN1.parse(asn1Encoded)) + } + + internal init(asn1Encoded: ArraySlice) throws { + self = try .init(asn1Encoded: ASN1.parse(asn1Encoded)) + } +} + +internal protocol ASN1Serializable { + func serialize(into coder: inout ASN1.Serializer) throws +} + +/// Covers ASN.1 types that may be implicitly tagged. Not all nodes can be! +internal protocol ASN1ImplicitlyTaggable: ASN1Parseable, ASN1Serializable { + /// The tag that the first node will use "by default" if the grammar omits + /// any more specific tag definition. + static var defaultIdentifier: ASN1.ASN1Identifier { get } + + init(asn1Encoded: ASN1.ASN1Node, withIdentifier identifier: ASN1.ASN1Identifier) throws + + func serialize(into coder: inout ASN1.Serializer, withIdentifier identifier: ASN1.ASN1Identifier) throws +} + +extension ASN1ImplicitlyTaggable { + internal init(asn1Encoded sequenceNodeIterator: inout ASN1.ASN1NodeCollection.Iterator, + withIdentifier identifier: ASN1.ASN1Identifier = Self.defaultIdentifier) throws { + guard let node = sequenceNodeIterator.next() else { + throw CryptoKitASN1Error.invalidASN1Object + } + + self = try .init(asn1Encoded: node, withIdentifier: identifier) + } + + internal init(asn1Encoded: [UInt8], withIdentifier identifier: ASN1.ASN1Identifier = Self.defaultIdentifier) throws { + self = try .init(asn1Encoded: ASN1.parse(asn1Encoded), withIdentifier: identifier) + } + + internal init(asn1Encoded: ArraySlice, withIdentifier identifier: ASN1.ASN1Identifier = Self.defaultIdentifier) throws { + self = try .init(asn1Encoded: ASN1.parse(asn1Encoded), withIdentifier: identifier) + } + + init(asn1Encoded: ASN1.ASN1Node) throws { + try self.init(asn1Encoded: asn1Encoded, withIdentifier: Self.defaultIdentifier) + } + + func serialize(into coder: inout ASN1.Serializer) throws { + try self.serialize(into: &coder, withIdentifier: Self.defaultIdentifier) + } +} + +extension ArraySlice where Element == UInt8 { + fileprivate mutating func readASN1Length() throws -> UInt? { + guard let firstByte = self.popFirst() else { + return nil + } + + switch firstByte { + case 0x80: + // Indefinite form. Unsupported. + throw CryptoKitASN1Error.unsupportedFieldLength + case let val where val & 0x80 == 0x80: + // Top bit is set, this is the long form. The remaining 7 bits of this octet + // determine how long the length field is. + let fieldLength = Int(val & 0x7F) + guard self.count >= fieldLength else { + return nil + } + + // We need to read the length bytes + let lengthBytes = self.prefix(fieldLength) + self = self.dropFirst(fieldLength) + let length = try UInt(bigEndianBytes: lengthBytes) + + // DER requires that we enforce that the length field was encoded in the minimum number of octets necessary. + let requiredBits = UInt.bitWidth - length.leadingZeroBitCount + switch requiredBits { + case 0...7: + // For 0 to 7 bits, the long form is unnacceptable and we require the short. + throw CryptoKitASN1Error.unsupportedFieldLength + case 8...: + // For 8 or more bits, fieldLength should be the minimum required. + let requiredBytes = (requiredBits + 7) / 8 + if fieldLength > requiredBytes { + throw CryptoKitASN1Error.unsupportedFieldLength + } + default: + // This is not reachable, but we'll error anyway. + throw CryptoKitASN1Error.unsupportedFieldLength + } + + return length + case let val: + // Short form, the length is only one 7-bit integer. + return UInt(val) + } + } +} + +extension FixedWidthInteger { + internal init(bigEndianBytes bytes: Bytes) throws where Bytes.Element == UInt8 { + guard bytes.count <= (Self.bitWidth / 8) else { + throw CryptoKitASN1Error.invalidASN1Object + } + + self = 0 + let shiftSizes = stride(from: 0, to: bytes.count * 8, by: 8).reversed() + + var index = bytes.startIndex + for shift in shiftSizes { + self |= Self(truncatingIfNeeded: bytes[index]) << shift + bytes.formIndex(after: &index) + } + } +} + +extension Array where Element == UInt8 { + fileprivate mutating func writeIdentifier(_ identifier: ASN1.ASN1Identifier) { + self.append(identifier.baseTag) + } + + fileprivate mutating func moveRange(offset: Int, range: Range) { + // We only bothered to implement this for positive offsets for now, the algorithm + // generalises. + precondition(offset > 0) + + let distanceFromEndOfRangeToEndOfSelf = self.distance(from: range.endIndex, to: self.endIndex) + if distanceFromEndOfRangeToEndOfSelf < offset { + // We begin by writing some zeroes out to the size we need. + for _ in 0..<(offset - distanceFromEndOfRangeToEndOfSelf) { + self.append(0) + } + } + + // Now we walk the range backwards, moving the elements. + for index in range.reversed() { + self[index + offset] = self[index] + } + } +} + +extension Int { + fileprivate var bytesNeededToEncode: Int { + // ASN.1 lengths are in two forms. If we can store the length in 7 bits, we should: + // that requires only one byte. Otherwise, we need multiple bytes: work out how many, + // plus one for the length of the length bytes. + if self <= 0x7F { + return 1 + } else { + // We need to work out how many bytes we need. There are many fancy bit-twiddling + // ways of doing this, but honestly we don't do this enough to need them, so we'll + // do it the easy way. This math is done on UInt because it makes the shift semantics clean. + // We save a branch here because we can never overflow this addition. + return UInt(self).neededBytes &+ 1 + } + } +} + +extension FixedWidthInteger { + // Bytes needed to store a given integer. + internal var neededBytes: Int { + let neededBits = self.bitWidth - self.leadingZeroBitCount + return (neededBits + 7) / 8 + } +} + +#endif // Linux or !SwiftPM diff --git a/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Any.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Any.swift new file mode 100644 index 0000000..2228f24 --- /dev/null +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Any.swift @@ -0,0 +1,70 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +#if (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API +@_exported import CryptoKit +#else +import Foundation + +extension ASN1 { + /// An ASN1 ANY represents...well, anything. + /// + /// In this case we store the ASN.1 ANY as a serialized representation. This is a bit annoying, + /// but it's the only safe way to manage this data, as we cannot arbitrarily parse it. + /// + /// The only things we allow users to do with ASN.1 ANYs is to try to decode them as something else, + /// to create them from something else, or to serialize them. + struct ASN1Any: ASN1Parseable, ASN1Serializable, Hashable { + fileprivate var serializedBytes: ArraySlice + + init(erasing: ASN1Type) throws { + var serializer = ASN1.Serializer() + try erasing.serialize(into: &serializer) + self.serializedBytes = ArraySlice(serializer.serializedBytes) + } + + init(erasing: ASN1Type, withIdentifier identifier: ASN1.ASN1Identifier) throws { + var serializer = ASN1.Serializer() + try erasing.serialize(into: &serializer, withIdentifier: identifier) + self.serializedBytes = ArraySlice(serializer.serializedBytes) + } + + init(asn1Encoded rootNode: ASN1.ASN1Node) { + // This is a bit sad: we just re-serialize this data. In an ideal world + // we'd update the parse representation so that all nodes can point at their + // complete backing storage, but for now this is better. + var serializer = ASN1.Serializer() + serializer.serialize(rootNode) + self.serializedBytes = ArraySlice(serializer.serializedBytes) + } + + func serialize(into coder: inout ASN1.Serializer) throws { + // Dangerous to just reach in there like this, but it's the right way to serialize this. + coder.serializedBytes.append(contentsOf: self.serializedBytes) + } + } +} + +extension ASN1Parseable { + init(asn1Any: ASN1.ASN1Any) throws { + try self.init(asn1Encoded: asn1Any.serializedBytes) + } +} + +extension ASN1ImplicitlyTaggable { + init(asn1Any: ASN1.ASN1Any, withIdentifier identifier: ASN1.ASN1Identifier) throws { + try self.init(asn1Encoded: asn1Any.serializedBytes, withIdentifier: identifier) + } +} + +#endif // Linux or !SwiftPM diff --git a/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1BitString.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1BitString.swift new file mode 100644 index 0000000..80e694d --- /dev/null +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1BitString.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +#if (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API +@_exported import CryptoKit +#else +import Foundation + +extension ASN1 { + /// A bitstring is a representation of...well...some bits. + struct ASN1BitString: ASN1ImplicitlyTaggable { + static var defaultIdentifier: ASN1.ASN1Identifier { + .primitiveBitString + } + + var bytes: ArraySlice + + init(asn1Encoded node: ASN1.ASN1Node, withIdentifier identifier: ASN1.ASN1Identifier) throws { + guard node.identifier == identifier else { + throw CryptoKitASN1Error.unexpectedFieldType + } + + guard case .primitive(let content) = node.content else { + preconditionFailure("ASN.1 parser generated primitive node with constructed content") + } + + // The initial octet explains how many of the bits in the _final_ octet are not part of the bitstring. + // The only value we support here is 0. + guard content.first == 0 else { + throw CryptoKitASN1Error.invalidASN1Object + } + + self.bytes = content.dropFirst() + } + + init(bytes: ArraySlice) { + self.bytes = bytes + } + + func serialize(into coder: inout ASN1.Serializer, withIdentifier identifier: ASN1.ASN1Identifier) throws { + coder.appendPrimitiveNode(identifier: identifier) { bytes in + bytes.append(0) + bytes.append(contentsOf: self.bytes) + } + } + } +} + +extension ASN1.ASN1BitString: Hashable { } + +extension ASN1.ASN1BitString: ContiguousBytes { + func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + return try self.bytes.withUnsafeBytes(body) + } +} + +#endif // Linux or !SwiftPM diff --git a/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Boolean.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Boolean.swift new file mode 100644 index 0000000..3fd156f --- /dev/null +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Boolean.swift @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +#if (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API +@_exported import CryptoKit +#else +import Foundation + +extension Bool: ASN1ImplicitlyTaggable { + static var defaultIdentifier: ASN1.ASN1Identifier { + .boolean + } + + init(asn1Encoded node: ASN1.ASN1Node, withIdentifier identifier: ASN1.ASN1Identifier) throws { + guard node.identifier == identifier else { + throw CryptoKitASN1Error.invalidASN1Object + } + + guard case .primitive(let bytes) = node.content, bytes.count == 1 else { + throw CryptoKitASN1Error.invalidASN1Object + } + + switch bytes[bytes.startIndex] { + case 0: + // Boolean false + self = false + case 0xff: + // Boolean true in DER + self = true + default: + // If we come to support BER then these values are all "true" as well. + throw CryptoKitASN1Error.invalidASN1Object + } + } + + func serialize(into coder: inout ASN1.Serializer, withIdentifier identifier: ASN1.ASN1Identifier) throws { + coder.appendPrimitiveNode(identifier: identifier) { bytes in + if self { + bytes.append(0xff) + } else { + bytes.append(0) + } + } + } +} + +#endif // Linux or !SwiftPM diff --git a/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Identifier.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Identifier.swift new file mode 100644 index 0000000..a350846 --- /dev/null +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Identifier.swift @@ -0,0 +1,146 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +#if (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API +@_exported import CryptoKit +#else +import Foundation + +extension ASN1 { + /// An `ASN1Identifier` is a representation of the abstract notion of an ASN.1 identifier. Identifiers have a number of properties that relate to both the specific + /// tag number as well as the properties of the identifier in the stream. + internal struct ASN1Identifier { + /// The base tag. In a general ASN.1 implementation we'd need an arbitrary precision integer here as the tag number can be arbitrarily large, but + /// we don't need the full generality here. + private(set) var baseTag: UInt8 + + /// Whether this tag is primitive. + var primitive: Bool { + return self.baseTag & 0x20 == 0 + } + + /// Whether this tag is constructed. + var constructed: Bool { + return !self.primitive + } + + enum TagClass { + case universal + case application + case contextSpecific + case `private` + } + + /// The class of this tag. + var tagClass: TagClass { + switch self.baseTag >> 6 { + case 0x00: + return .universal + case 0x01: + return .application + case 0x02: + return .contextSpecific + case 0x03: + return .private + default: + fatalError("Unreachable") + } + } + + init(rawIdentifier: UInt8) throws { + // We don't support multibyte identifiers, which are signalled when the bottom 5 bits are all 1. + guard rawIdentifier & 0x1F != 0x1F else { + throw CryptoKitASN1Error.invalidFieldIdentifier + } + + self.baseTag = rawIdentifier + } + + init(explicitTagWithNumber number: Int, tagClass: TagClass) { + precondition(number >= 0) + precondition(number < 0x1F) + + self.baseTag = UInt8(number) + + switch tagClass { + case .universal: + preconditionFailure("Explicit tags may not be universal") + case .application: + self.baseTag |= 1 << 6 + case .contextSpecific: + self.baseTag |= 2 << 6 + case .private: + self.baseTag |= 3 << 6 + } + + // Explicit tags are always constructed. + self.baseTag |= 0x20 + } + + init(tagWithNumber number: Int, tagClass: TagClass, constructed: Bool) { + precondition(number >= 0) + precondition(number < 0x1F) + + self.baseTag = UInt8(number) + + switch tagClass { + case .universal: + preconditionFailure("Custom tags may not be universal") + case .application: + self.baseTag |= 1 << 6 + case .contextSpecific: + self.baseTag |= 2 << 6 + case .private: + self.baseTag |= 3 << 6 + } + + if constructed { + self.baseTag |= 0x20 + } + } + } +} + +extension ASN1.ASN1Identifier { + internal static let objectIdentifier = try! ASN1.ASN1Identifier(rawIdentifier: 0x06) + internal static let primitiveBitString = try! ASN1.ASN1Identifier(rawIdentifier: 0x03) + internal static let primitiveOctetString = try! ASN1.ASN1Identifier(rawIdentifier: 0x04) + internal static let integer = try! ASN1.ASN1Identifier(rawIdentifier: 0x02) + internal static let sequence = try! ASN1.ASN1Identifier(rawIdentifier: 0x30) + internal static let set = try! ASN1.ASN1Identifier(rawIdentifier: 0x31) + internal static let null = try! ASN1.ASN1Identifier(rawIdentifier: 0x05) + internal static let boolean = try! ASN1.ASN1Identifier(rawIdentifier: 0x01) + internal static let enumerated = try! ASN1.ASN1Identifier(rawIdentifier: 0x0a) + internal static let primitiveUTF8String = try! ASN1.ASN1Identifier(rawIdentifier: 0x0c) + internal static let primitiveNumericString = try! ASN1.ASN1Identifier(rawIdentifier: 0x12) + internal static let primitivePrintableString = try! ASN1.ASN1Identifier(rawIdentifier: 0x13) + internal static let primitiveTeletexString = try! ASN1.ASN1Identifier(rawIdentifier: 0x14) + internal static let primitiveVideotexString = try! ASN1.ASN1Identifier(rawIdentifier: 0x15) + internal static let primitiveIA5String = try! ASN1.ASN1Identifier(rawIdentifier: 0x16) + internal static let primitiveGraphicString = try! ASN1.ASN1Identifier(rawIdentifier: 0x19) + internal static let primitiveVisibleString = try! ASN1.ASN1Identifier(rawIdentifier: 0x1a) + internal static let primitiveGeneralString = try! ASN1.ASN1Identifier(rawIdentifier: 0x1b) + internal static let primitiveUniversalString = try! ASN1.ASN1Identifier(rawIdentifier: 0x1c) + internal static let primitiveBMPString = try! ASN1.ASN1Identifier(rawIdentifier: 0x1e) + internal static let generalizedTime = try! ASN1.ASN1Identifier(rawIdentifier: 0x18) +} + +extension ASN1.ASN1Identifier: Hashable { } + +extension ASN1.ASN1Identifier: CustomStringConvertible { + var description: String { + return "ASN1Identifier(\(self.baseTag))" + } +} + +#endif // Linux or !SwiftPM diff --git a/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Integer.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Integer.swift new file mode 100644 index 0000000..3ef9cf6 --- /dev/null +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Integer.swift @@ -0,0 +1,272 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +#if (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API +@_exported import CryptoKit +#else +import Foundation + +/// A protocol that represents any internal object that can present itself as a INTEGER, or be parsed from +/// a INTEGER. +/// +/// This is not a very good solution for a fully-fledged ASN.1 library: we'd rather have a better numerics +/// protocol that could both initialize from and serialize to either bytes or words. However, no such +/// protocol exists today. +protocol ASN1IntegerRepresentable: ASN1ImplicitlyTaggable { + associatedtype IntegerBytes: RandomAccessCollection where IntegerBytes.Element == UInt8 + + /// Whether this type can represent signed integers. If this is set to false, the serializer and + /// parser will automatically handle padding with leading zero bytes as needed. + static var isSigned: Bool { get } + + init(asn1IntegerBytes: ArraySlice) throws + + func withBigEndianIntegerBytes(_ body: (IntegerBytes) throws -> ReturnType) rethrows -> ReturnType +} + +extension ASN1IntegerRepresentable { + static var defaultIdentifier: ASN1.ASN1Identifier { + .integer + } + + internal init(asn1Encoded node: ASN1.ASN1Node, withIdentifier identifier: ASN1.ASN1Identifier) throws { + guard node.identifier == identifier else { + throw CryptoKitASN1Error.unexpectedFieldType + } + + guard case .primitive(var dataBytes) = node.content else { + preconditionFailure("ASN.1 parser generated primitive node with constructed content") + } + + // Zero bytes of integer is not an acceptable encoding. + guard dataBytes.count > 0 else { + throw CryptoKitASN1Error.invalidASN1IntegerEncoding + } + + // 8.3.2 If the contents octets of an integer value encoding consist of more than one octet, then the bits of the first octet and bit 8 of the second octet: + // + // a) shall not all be ones; and + // b) shall not all be zero. + // + // NOTE – These rules ensure that an integer value is always encoded in the smallest possible number of octets. + if let first = dataBytes.first, let second = dataBytes.dropFirst().first { + if (first == 0xFF) && second.topBitSet || + (first == 0x00) && !second.topBitSet { + throw CryptoKitASN1Error.invalidASN1IntegerEncoding + } + } + + // If the type we're trying to decode is unsigned, and the top byte is zero, we should strip it. + // If the top bit is set, however, this is an invalid conversion: the number needs to be positive! + if !Self.isSigned, let first = dataBytes.first { + if first == 0x00 { + dataBytes = dataBytes.dropFirst() + } else if first & 0x80 == 0x80 { + throw CryptoKitASN1Error.invalidASN1IntegerEncoding + } + } + + self = try Self(asn1IntegerBytes: dataBytes) + } + + internal func serialize(into coder: inout ASN1.Serializer, withIdentifier identifier: ASN1.ASN1Identifier) throws { + coder.appendPrimitiveNode(identifier: identifier) { bytes in + self.withBigEndianIntegerBytes { integerBytes in + // If the number of bytes is 0, we're encoding a zero. That actually _does_ require one byte. + if integerBytes.count == 0 { + bytes.append(0) + return + } + + // If self is unsigned and the first byte has the top bit set, we need to prepend a 0 byte. + if !Self.isSigned, let topByte = integerBytes.first, topByte.topBitSet { + bytes.append(0) + bytes.append(contentsOf: integerBytes) + } else { + // Either self is signed, or the top bit isn't set. Either way, trim to make sure the representation is minimal. + bytes.append(contentsOf: integerBytes.trimLeadingExcessBytes()) + } + } + } + } +} + +// MARK: - Auto-conformance for FixedWidthInteger with fixed width magnitude. +extension ASN1IntegerRepresentable where Self: FixedWidthInteger { + init(asn1IntegerBytes bytes: ArraySlice) throws { + // Defer to the FixedWidthInteger constructor. + // There's a wrinkle here: if this is a signed integer, and the top bit of the data bytes was set, + // then we need to 1-extend the bytes. This is because ASN.1 tries to delete redundant bytes that + // are all 1. + self = try Self(bigEndianBytes: bytes) + + if Self.isSigned, let first = bytes.first, first.topBitSet { + for shift in stride(from: self.bitWidth - self.leadingZeroBitCount, to: self.bitWidth, by: 8) { + self |= 0xFF << shift + } + } + } + + func withBigEndianIntegerBytes(_ body: (IntegerBytesCollection) throws -> ReturnType) rethrows -> ReturnType { + return try body(IntegerBytesCollection(self)) + } +} + +struct IntegerBytesCollection { + private var integer: Integer + + init(_ integer: Integer) { + self.integer = integer + } +} + +extension IntegerBytesCollection: RandomAccessCollection { + struct Index { + fileprivate var byteNumber: Int + + fileprivate init(byteNumber: Int) { + self.byteNumber = byteNumber + } + + fileprivate var shift: Integer { + // As byte number 0 is the end index, the byte number is one byte too large for the shift. + return Integer((self.byteNumber - 1) * 8) + } + } + + var startIndex: Index { + return Index(byteNumber: Int(self.integer.neededBytes)) + } + + var endIndex: Index { + return Index(byteNumber: 0) + } + + var count: Int { + return Int(self.integer.neededBytes) + } + + subscript(index: Index) -> UInt8 { + // We perform the bitwise operations in magnitude space. + let shifted = Integer.Magnitude(truncatingIfNeeded: self.integer) >> index.shift + let masked = shifted & 0xFF + return UInt8(masked) + } +} + +extension IntegerBytesCollection.Index: Equatable { } + +extension IntegerBytesCollection.Index: Comparable { + // Comparable here is backwards to the original ordering. + static func <(lhs: Self, rhs: Self) -> Bool { + return lhs.byteNumber > rhs.byteNumber + } + + static func >(lhs: Self, rhs: Self) -> Bool { + return lhs.byteNumber < rhs.byteNumber + } + + static func <=(lhs: Self, rhs: Self) -> Bool { + return lhs.byteNumber >= rhs.byteNumber + } + + static func >=(lhs: Self, rhs: Self) -> Bool { + return lhs.byteNumber <= rhs.byteNumber + } +} + +extension IntegerBytesCollection.Index: Strideable { + func advanced(by n: Int) -> IntegerBytesCollection.Index { + return IntegerBytesCollection.Index(byteNumber: self.byteNumber - n) + } + + func distance(to other: IntegerBytesCollection.Index) -> Int { + // Remember that early indices have high byte numbers and later indices have low ones. + return self.byteNumber - other.byteNumber + } +} + +extension Int8: ASN1IntegerRepresentable { } + +extension UInt8: ASN1IntegerRepresentable { } + +extension Int16: ASN1IntegerRepresentable { } + +extension UInt16: ASN1IntegerRepresentable { } + +extension Int32: ASN1IntegerRepresentable { } + +extension UInt32: ASN1IntegerRepresentable { } + +extension Int64: ASN1IntegerRepresentable { } + +extension UInt64: ASN1IntegerRepresentable { } + +extension Int: ASN1IntegerRepresentable { } + +extension UInt: ASN1IntegerRepresentable { } + +extension RandomAccessCollection where Element == UInt8 { + fileprivate func trimLeadingExcessBytes() -> SubSequence { + var slice = self[...] + guard let first = slice.first else { + // Easy case, empty. + return slice + } + + let wholeByte: UInt8 + + switch first { + case 0: + wholeByte = 0 + case 0xFF: + wholeByte = 0xFF + default: + // We're already fine, this is maximally compact. We need the whole thing. + return slice + } + + // We never trim this to less than one byte, as that's always the smallest representation. + while slice.count > 1 { + // If the first byte is equal to our original first byte, and the top bit + // of the next byte is also equal to that, then we need to drop the byte and + // go again. + if slice.first != wholeByte { + break + } + + guard let second = slice.dropFirst().first else { + preconditionFailure("Loop condition violated: must be at least two bytes left") + } + + if second & 0x80 != wholeByte & 0x80 { + // Different top bit, we need the leading byte. + break + } + + // Both the first byte and the top bit of the next are all zero or all 1, drop the leading + // byte. + slice = slice.dropFirst() + } + + return slice + } +} + +extension UInt8 { + fileprivate var topBitSet: Bool { + return (self & 0x80) != 0 + } +} + +#endif // Linux or !SwiftPM diff --git a/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Null.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Null.swift new file mode 100644 index 0000000..0dcb788 --- /dev/null +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Null.swift @@ -0,0 +1,44 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +#if (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API +@_exported import CryptoKit +#else +import Foundation + +extension ASN1 { + /// An ASN1 NULL represents nothing. + struct ASN1Null: ASN1ImplicitlyTaggable, Hashable { + static var defaultIdentifier: ASN1.ASN1Identifier { + .null + } + + init() { } + + init(asn1Encoded node: ASN1.ASN1Node, withIdentifier identifier: ASN1.ASN1Identifier) throws { + guard node.identifier == identifier, case .primitive(let content) = node.content else { + throw CryptoKitASN1Error.unexpectedFieldType + } + + guard content.count == 0 else { + throw CryptoKitASN1Error.invalidASN1Object + } + } + + func serialize(into coder: inout ASN1.Serializer, withIdentifier identifier: ASN1.ASN1Identifier) { + coder.appendPrimitiveNode(identifier: identifier, { _ in }) + } + } +} + +#endif // Linux or !SwiftPM diff --git a/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1OctetString.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1OctetString.swift new file mode 100644 index 0000000..d1a6d49 --- /dev/null +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1OctetString.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +#if (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API +@_exported import CryptoKit +#else +import Foundation + +extension ASN1 { + /// An octet string is a representation of a string of octets. + struct ASN1OctetString: ASN1ImplicitlyTaggable { + static var defaultIdentifier: ASN1.ASN1Identifier { + .primitiveOctetString + } + + var bytes: ArraySlice + + init(asn1Encoded node: ASN1.ASN1Node, withIdentifier identifier: ASN1.ASN1Identifier) throws { + guard node.identifier == identifier else { + throw CryptoKitASN1Error.unexpectedFieldType + } + + guard case .primitive(let content) = node.content else { + preconditionFailure("ASN.1 parser generated primitive node with constructed content") + } + + self.bytes = content + } + + init(contentBytes: ArraySlice) { + self.bytes = contentBytes + } + + func serialize(into coder: inout ASN1.Serializer, withIdentifier identifier: ASN1.ASN1Identifier) throws { + coder.appendPrimitiveNode(identifier: identifier) { bytes in + bytes.append(contentsOf: self.bytes) + } + } + } +} + +extension ASN1.ASN1OctetString: Hashable { } + +extension ASN1.ASN1OctetString: ContiguousBytes { + func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + return try self.bytes.withUnsafeBytes(body) + } +} + +#endif // Linux or !SwiftPM diff --git a/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Strings.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Strings.swift new file mode 100644 index 0000000..c57dc2e --- /dev/null +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ASN1Strings.swift @@ -0,0 +1,196 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +#if (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API +@_exported import CryptoKit +#else +import Foundation + +extension ASN1 { + /// A UTF8String is roughly what it sounds like. We note that all the string types are encoded as implicitly tagged + /// octet strings, and so for now we just piggyback on the decoder and encoder for that type. + struct ASN1UTF8String: ASN1ImplicitlyTaggable, Hashable, ContiguousBytes, ExpressibleByStringLiteral { + static var defaultIdentifier: ASN1.ASN1Identifier { + .primitiveUTF8String + } + + var bytes: ArraySlice + + init(asn1Encoded node: ASN1.ASN1Node, withIdentifier identifier: ASN1.ASN1Identifier) throws { + self.bytes = try ASN1OctetString(asn1Encoded: node, withIdentifier: identifier).bytes + } + + init(contentBytes: ArraySlice) { + self.bytes = contentBytes + } + + init(stringLiteral value: StringLiteralType) { + self.bytes = ArraySlice(value.utf8) + } + + init(_ string: String) { + self.bytes = ArraySlice(string.utf8) + } + + func serialize(into coder: inout ASN1.Serializer, withIdentifier identifier: ASN1.ASN1Identifier) throws { + let octet = ASN1OctetString(contentBytes: self.bytes) + try octet.serialize(into: &coder, withIdentifier: identifier) + } + + func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + return try self.bytes.withUnsafeBytes(body) + } + } + + /// We note that all the string types are encoded as implicitly tagged + /// octet strings, and so for now we just piggyback on the decoder and encoder for that type. + struct ASN1TeletexString: ASN1ImplicitlyTaggable, Hashable, ContiguousBytes, ExpressibleByStringLiteral { + static var defaultIdentifier: ASN1.ASN1Identifier { + .primitiveTeletexString + } + + var bytes: ArraySlice + + init(asn1Encoded node: ASN1.ASN1Node, withIdentifier identifier: ASN1.ASN1Identifier) throws { + self.bytes = try ASN1OctetString(asn1Encoded: node, withIdentifier: identifier).bytes + } + + init(contentBytes: ArraySlice) { + self.bytes = contentBytes + } + + init(stringLiteral value: StringLiteralType) { + self.bytes = ArraySlice(value.utf8) + } + + init(_ string: String) { + self.bytes = ArraySlice(string.utf8) + } + + func serialize(into coder: inout ASN1.Serializer, withIdentifier identifier: ASN1.ASN1Identifier) throws { + let octet = ASN1OctetString(contentBytes: self.bytes) + try octet.serialize(into: &coder, withIdentifier: identifier) + } + + func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + return try self.bytes.withUnsafeBytes(body) + } + } + + /// We note that all the string types are encoded as implicitly tagged + /// octet strings, and so for now we just piggyback on the decoder and encoder for that type. + struct ASN1PrintableString: ASN1ImplicitlyTaggable, Hashable, ContiguousBytes, ExpressibleByStringLiteral { + static var defaultIdentifier: ASN1.ASN1Identifier { + .primitivePrintableString + } + + var bytes: ArraySlice + + init(asn1Encoded node: ASN1.ASN1Node, withIdentifier identifier: ASN1.ASN1Identifier) throws { + self.bytes = try ASN1OctetString(asn1Encoded: node, withIdentifier: identifier).bytes + } + + init(contentBytes: ArraySlice) { + self.bytes = contentBytes + } + + init(stringLiteral value: StringLiteralType) { + self.bytes = ArraySlice(value.utf8) + } + + init(_ string: String) { + self.bytes = ArraySlice(string.utf8) + } + + func serialize(into coder: inout ASN1.Serializer, withIdentifier identifier: ASN1.ASN1Identifier) throws { + let octet = ASN1OctetString(contentBytes: self.bytes) + try octet.serialize(into: &coder, withIdentifier: identifier) + } + + func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + return try self.bytes.withUnsafeBytes(body) + } + } + + /// We note that all the string types are encoded as implicitly tagged + /// octet strings, and so for now we just piggyback on the decoder and encoder for that type. + struct ASN1UniversalString: ASN1ImplicitlyTaggable, Hashable, ContiguousBytes, ExpressibleByStringLiteral { + static var defaultIdentifier: ASN1.ASN1Identifier { + .primitiveUniversalString + } + + var bytes: ArraySlice + + init(asn1Encoded node: ASN1.ASN1Node, withIdentifier identifier: ASN1.ASN1Identifier) throws { + self.bytes = try ASN1OctetString(asn1Encoded: node, withIdentifier: identifier).bytes + } + + init(contentBytes: ArraySlice) { + self.bytes = contentBytes + } + + init(stringLiteral value: StringLiteralType) { + self.bytes = ArraySlice(value.utf8) + } + + init(_ string: String) { + self.bytes = ArraySlice(string.utf8) + } + + func serialize(into coder: inout ASN1.Serializer, withIdentifier identifier: ASN1.ASN1Identifier) throws { + let octet = ASN1OctetString(contentBytes: self.bytes) + try octet.serialize(into: &coder, withIdentifier: identifier) + } + + func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + return try self.bytes.withUnsafeBytes(body) + } + } + + /// We note that all the string types are encoded as implicitly tagged + /// octet strings, and so for now we just piggyback on the decoder and encoder for that type. + struct ASN1BMPString: ASN1ImplicitlyTaggable, Hashable, ContiguousBytes, ExpressibleByStringLiteral { + static var defaultIdentifier: ASN1.ASN1Identifier { + .primitiveBMPString + } + + var bytes: ArraySlice + + init(asn1Encoded node: ASN1.ASN1Node, withIdentifier identifier: ASN1.ASN1Identifier) throws { + self.bytes = try ASN1OctetString(asn1Encoded: node, withIdentifier: identifier).bytes + } + + init(contentBytes: ArraySlice) { + self.bytes = contentBytes + } + + init(stringLiteral value: StringLiteralType) { + self.bytes = ArraySlice(value.utf8) + } + + init(_ string: String) { + self.bytes = ArraySlice(string.utf8) + } + + func serialize(into coder: inout ASN1.Serializer, withIdentifier identifier: ASN1.ASN1Identifier) throws { + let octet = ASN1OctetString(contentBytes: self.bytes) + try octet.serialize(into: &coder, withIdentifier: identifier) + } + + func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + return try self.bytes.withUnsafeBytes(body) + } + } +} + +#endif // Linux or !SwiftPM diff --git a/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ArraySliceBigint.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ArraySliceBigint.swift new file mode 100644 index 0000000..22cab28 --- /dev/null +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ArraySliceBigint.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +#if (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API +@_exported import CryptoKit +#else + +// For temporary purposes we pretend that ArraySlice is our "bigint" type. We don't really need anything else. +extension ArraySlice: ASN1Serializable where Element == UInt8 { } + +extension ArraySlice: ASN1Parseable where Element == UInt8 { } + +extension ArraySlice: ASN1ImplicitlyTaggable where Element == UInt8 { } + +extension ArraySlice: ASN1IntegerRepresentable where Element == UInt8 { + // We only use unsigned "bigint"s + static var isSigned: Bool { + return false + } + + init(asn1IntegerBytes: ArraySlice) throws { + self = asn1IntegerBytes + } + + func withBigEndianIntegerBytes(_ body: (ArraySlice) throws -> ReturnType) rethrows -> ReturnType { + return try body(self) + } +} + +#endif // Linux or !SwiftPM diff --git a/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/GeneralizedTime.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/GeneralizedTime.swift new file mode 100644 index 0000000..cb423a4 --- /dev/null +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/GeneralizedTime.swift @@ -0,0 +1,384 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +#if (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API +@_exported import CryptoKit +#else +import Foundation + +extension ASN1 { + struct GeneralizedTime: ASN1ImplicitlyTaggable, Hashable { + static var defaultIdentifier: ASN1.ASN1Identifier { + .generalizedTime + } + + var year: Int { + get { + return self._year + } + set { + self._year = newValue + try! self.validate() + } + } + + var month: Int { + get { + return self._month + } + set { + self._month = newValue + try! self.validate() + } + } + + var day: Int { + get { + return self._day + } + set { + self._day = newValue + try! self.validate() + } + } + + var hours: Int { + get { + return self._hours + } + set { + self._hours = newValue + try! self.validate() + } + } + + var minutes: Int { + get { + return self._minutes + } + set { + self._minutes = newValue + try! self.validate() + } + } + + var seconds: Int { + get { + return self._seconds + } + set { + self._seconds = newValue + try! self.validate() + } + } + + var fractionalSeconds: Double { + get { + return self._fractionalSeconds + } + set { + self._fractionalSeconds = newValue + try! self.validate() + } + } + + private var _year: Int + private var _month: Int + private var _day: Int + private var _hours: Int + private var _minutes: Int + private var _seconds: Int + private var _fractionalSeconds: Double + + init(year: Int, month: Int, day: Int, hours: Int, minutes: Int, seconds: Int, fractionalSeconds: Double) throws { + self._year = year + self._month = month + self._day = day + self._hours = hours + self._minutes = minutes + self._seconds = seconds + self._fractionalSeconds = fractionalSeconds + + try self.validate() + } + + init(asn1Encoded node: ASN1.ASN1Node, withIdentifier identifier: ASN1.ASN1Identifier) throws { + guard node.identifier == identifier else { + throw CryptoKitASN1Error.unexpectedFieldType + } + + guard case .primitive(let content) = node.content else { + preconditionFailure("ASN.1 parser generated primitive node with constructed content") + } + + self = try .parseDateBytes(content) + } + + func serialize(into coder: inout ASN1.Serializer, withIdentifier identifier: ASN1.ASN1Identifier) throws { + coder.appendPrimitiveNode(identifier: identifier) { bytes in + bytes.append(self) + } + } + + private func validate() throws { + // Validate that the structure is well-formed. + guard self._year >= 0 && self._year <= 9999 else { + throw CryptoKitASN1Error.invalidASN1Object + } + + // This also validates the month. + guard let daysInMonth = ASN1.GeneralizedTime.daysInMonth(self._month, ofYear: self._year) else { + throw CryptoKitASN1Error.invalidASN1Object + } + + guard self._day >= 1 && self._day <= daysInMonth else { + throw CryptoKitASN1Error.invalidASN1Object + } + + guard self._hours >= 0 && self._hours < 24 else { + throw CryptoKitASN1Error.invalidASN1Object + } + + guard self._minutes >= 0 && self._minutes < 60 else { + throw CryptoKitASN1Error.invalidASN1Object + } + + // We allow leap seconds here, but don't validate it. + // This exposes us to potential confusion if we naively implement + // comparison here. We should consider whether this needs to be transformable + // to `Date` or similar. + guard self._seconds >= 0 && self._seconds <= 61 else { + throw CryptoKitASN1Error.invalidASN1Object + } + + // Fractional seconds may not be negative and may not be 1 or more. + guard self._fractionalSeconds >= 0 && self._fractionalSeconds < 1 else { + throw CryptoKitASN1Error.invalidASN1Object + } + } + } +} + +extension ASN1.GeneralizedTime { + fileprivate static func parseDateBytes(_ bytes: ArraySlice) throws -> ASN1.GeneralizedTime { + var bytes = bytes + + // First, there must always be a calendar date. No separators, 4 + // digits for the year, 2 digits for the month, 2 digits for the day. + guard let rawYear = bytes.readFourDigitDecimalInteger(), + let rawMonth = bytes.readTwoDigitDecimalInteger(), + let rawDay = bytes.readTwoDigitDecimalInteger() else { + throw CryptoKitASN1Error.invalidASN1Object + } + + // Next there must be a _time_. Per DER rules, this time must always go + // to at least seconds, there are no separators, there is no time-zone (but there must be a 'Z'), + // and there may be fractional seconds but they must not have trailing zeros. + guard let rawHour = bytes.readTwoDigitDecimalInteger(), + let rawMinutes = bytes.readTwoDigitDecimalInteger(), + let rawSeconds = bytes.readTwoDigitDecimalInteger() else { + throw CryptoKitASN1Error.invalidASN1Object + } + + // There may be some fractional seconds. + var fractionalSeconds: Double = 0 + if bytes.first == UInt8(ascii: ".") { + fractionalSeconds = try bytes.readFractionalSeconds() + } + + // The next character _must_ be Z, or the encoding is invalid. + guard bytes.popFirst() == UInt8(ascii: "Z") else { + throw CryptoKitASN1Error.invalidASN1Object + } + + // Great! There better not be anything left. + guard bytes.count == 0 else { + throw CryptoKitASN1Error.invalidASN1Object + } + + return try ASN1.GeneralizedTime(year: rawYear, + month: rawMonth, + day: rawDay, + hours: rawHour, + minutes: rawMinutes, + seconds: rawSeconds, + fractionalSeconds: fractionalSeconds) + } + + static func daysInMonth(_ month: Int, ofYear year: Int) -> Int? { + switch month { + case 1: + return 31 + case 2: + // This one has a dependency on the year! + // A leap year occurs in any year divisible by 4, except when that year is divisible by 100, + // unless the year is divisible by 400. + let isLeapYear = (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)) + return isLeapYear ? 29 : 28 + case 3: + return 31 + case 4: + return 30 + case 5: + return 31 + case 6: + return 30 + case 7: + return 31 + case 8: + return 31 + case 9: + return 30 + case 10: + return 31 + case 11: + return 30 + case 12: + return 31 + default: + return nil + } + } +} + +extension ArraySlice where Element == UInt8 { + fileprivate mutating func readFourDigitDecimalInteger() -> Int? { + guard let first = self.readTwoDigitDecimalInteger(), + let second = self.readTwoDigitDecimalInteger() else { + return nil + } + + // Unchecked math is still safe here: we're in Int32 space, and this number cannot + // get any larger than 9999. + return (first &* 100) &+ second + } + + fileprivate mutating func readTwoDigitDecimalInteger() -> Int? { + guard let firstASCII = self.popFirst(), + let secondASCII = self.popFirst() else { + return nil + } + + guard let first = Int(fromDecimalASCII: firstASCII), + let second = Int(fromDecimalASCII: secondASCII) else { + return nil + } + + // Unchecked math is safe here: we're in Int32 space at the very least, and this number cannot + // possibly be smaller than zero or larger than 99. + return (first &* 10) &+ (second) + } + + /// This may only be called if there's a leading period: we precondition on this fact. + fileprivate mutating func readFractionalSeconds() throws -> Double { + precondition(self.popFirst() == UInt8(ascii: ".")) + + var numerator = 0 + var denominator = 1 + + while let nextASCII = self.first, let next = Int(fromDecimalASCII: nextASCII) { + self = self.dropFirst() + + let (newNumerator, multiplyOverflow) = numerator.multipliedReportingOverflow(by: 10) + let (newDenominator, secondMultiplyOverflow) = denominator.multipliedReportingOverflow(by: 10) + let (newNumeratorWithAdded, addingOverflow) = newNumerator.addingReportingOverflow(next) + + // If the new denominator overflows, we just cap to the old value. + if !secondMultiplyOverflow { + denominator = newDenominator + } + + // If the numerator overflows, we don't support the result. + if multiplyOverflow || addingOverflow { + throw CryptoKitASN1Error.invalidASN1Object + } + + numerator = newNumeratorWithAdded + } + + // Ok, we're either at the end or the next character is a Z. One final check: there may not have + // been any trailing zeros here. This means the number may not be 0 mod 10. + if numerator % 10 == 0 { + throw CryptoKitASN1Error.invalidASN1Object + } + + return Double(numerator) / Double(denominator) + } +} + +extension Array where Element == UInt8 { + fileprivate mutating func append(_ generalizedTime: ASN1.GeneralizedTime) { + self.appendFourDigitDecimal(generalizedTime.year) + self.appendTwoDigitDecimal(generalizedTime.month) + self.appendTwoDigitDecimal(generalizedTime.day) + self.appendTwoDigitDecimal(generalizedTime.hours) + self.appendTwoDigitDecimal(generalizedTime.minutes) + self.appendTwoDigitDecimal(generalizedTime.seconds) + + // Ok, tricky moment here. Is the fractional part non-zero? If it is, we need to write it out as well. + if generalizedTime.fractionalSeconds != 0 { + let stringified = String(generalizedTime.fractionalSeconds) + assert(stringified.starts(with: "0.")) + + self.append(contentsOf: stringified.utf8.dropFirst(1)) + // Remove any trailing zeros from self, they are forbidden. + while self.last == 0 { + self = self.dropLast() + } + } + + self.append(UInt8(ascii: "Z")) + } + + fileprivate mutating func appendFourDigitDecimal(_ number: Int) { + assert(number >= 0 && number <= 9999) + + // Each digit can be isolated by dividing by the place and then taking the result modulo 10. + // This is annoyingly division heavy. There may be a better algorithm floating around. + // Unchecked math is fine, there cannot be an overflow here. + let asciiZero = UInt8(ascii: "0") + self.append(UInt8(truncatingIfNeeded: (number / 1000) % 10) &+ asciiZero) + self.append(UInt8(truncatingIfNeeded: (number / 100) % 10) &+ asciiZero) + self.append(UInt8(truncatingIfNeeded: (number / 10) % 10) &+ asciiZero) + self.append(UInt8(truncatingIfNeeded: number % 10) &+ asciiZero) + } + + fileprivate mutating func appendTwoDigitDecimal(_ number: Int) { + assert(number >= 0 && number <= 99) + + // Each digit can be isolated by dividing by the place and then taking the result modulo 10. + // This is annoyingly division heavy. There may be a better algorithm floating around. + // Unchecked math is fine, there cannot be an overflow here. + let asciiZero = UInt8(ascii: "0") + self.append(UInt8(truncatingIfNeeded: (number / 10) % 10) &+ asciiZero) + self.append(UInt8(truncatingIfNeeded: number % 10) &+ asciiZero) + } +} + +extension Int { + fileprivate init?(fromDecimalASCII ascii: UInt8) { + let asciiZero = UInt8(ascii: "0") + let zeroToNine = 0...9 + + // These are all coming from UInt8space, the subtraction cannot overflow. + let converted = Int(ascii) &- Int(asciiZero) + + guard zeroToNine.contains(converted) else { + return nil + } + + self = converted + } +} + +#endif // Linux or !SwiftPM diff --git a/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ObjectIdentifier.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ObjectIdentifier.swift new file mode 100644 index 0000000..850d999 --- /dev/null +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/Basic ASN1 Types/ObjectIdentifier.swift @@ -0,0 +1,222 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Changes by Alexander Cyon: +// * Removed ASN1 identifiers for swift-crypto curves +// * Added ASN1 identifier for curve `secp256k1` + +#if (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API +@_exported import CryptoKit +#else +import Foundation + +extension ASN1 { + /// An Object Identifier is a representation of some kind of object: really any kind of object. + /// + /// It represents a node in an OID hierarchy, and is usually represented as an ordered sequence of numbers. + /// + /// We mostly don't care about the semantics of the thing, we just care about being able to store and compare them. + struct ASN1ObjectIdentifier: ASN1ImplicitlyTaggable { + static var defaultIdentifier: ASN1.ASN1Identifier { + .objectIdentifier + } + + private var oidComponents: [UInt] + + init(asn1Encoded node: ASN1.ASN1Node, withIdentifier identifier: ASN1.ASN1Identifier) throws { + guard node.identifier == identifier else { + throw CryptoKitASN1Error.unexpectedFieldType + } + + guard case .primitive(var content) = node.content else { + preconditionFailure("ASN.1 parser generated primitive node with constructed content") + } + + // We have to parse the content. From the spec: + // + // > Each subidentifier is represented as a series of (one or more) octets. Bit 8 of each octet indicates whether it + // > is the last in the series: bit 8 of the last octet is zero, bit 8 of each preceding octet is one. Bits 7 to 1 of + // > the octets in the series collectively encode the subidentifier. Conceptually, these groups of bits are concatenated + // > to form an unsigned binary number whose most significant bit is bit 7 of the first octet and whose least significant + // > bit is bit 1 of the last octet. The subidentifier shall be encoded in the fewest possible octets[...]. + // > + // > The number of subidentifiers (N) shall be one less than the number of object identifier components in the object identifier + // > value being encoded. + // > + // > The numerical value of the first subidentifier is derived from the values of the first _two_ object identifier components + // > in the object identifier value being encoded, using the formula: + // > + // > (X*40) + Y + // > + // > where X is the value of the first object identifier component and Y is the value of the second object identifier component. + // + // Yeah, this is a bit bananas, but basically there are only 3 first OID components (0, 1, 2) and there are no more than 39 children + // of nodes 0 or 1. In my view this is too clever by half, but the ITU.T didn't ask for my opinion when they were coming up with this + // scheme, likely because I was in middle school at the time. + var subcomponents = [UInt]() + while content.count > 0 { + subcomponents.append(try content.readOIDSubidentifier()) + } + + guard subcomponents.count >= 2 else { + throw CryptoKitASN1Error.invalidObjectIdentifier + } + + // Now we need to expand the subcomponents out. This means we need to undo the step above. The first component will be in the range 0..<40 + // when the first oidComponent is 0, 40..<80 when the first oidComponent is 1, and 80+ when the first oidComponent is 2. + var oidComponents = [UInt]() + oidComponents.reserveCapacity(subcomponents.count + 1) + + switch subcomponents.first! { + case ..<40: + oidComponents.append(0) + oidComponents.append(subcomponents.first!) + case 40 ..< 80: + oidComponents.append(1) + oidComponents.append(subcomponents.first! - 40) + default: + oidComponents.append(2) + oidComponents.append(subcomponents.first! - 80) + } + + oidComponents.append(contentsOf: subcomponents.dropFirst()) + + self.oidComponents = oidComponents + } + + func serialize(into coder: inout ASN1.Serializer, withIdentifier identifier: ASN1.ASN1Identifier) throws { + coder.appendPrimitiveNode(identifier: identifier) { bytes in + var components = self.oidComponents[...] + guard let firstComponent = components.popFirst(), let secondComponent = components.popFirst() else { + preconditionFailure("Invalid number of OID components: must be at least two!") + } + + let serializedFirstComponent = (firstComponent * 40) + secondComponent + ASN1ObjectIdentifier.writeOIDSubidentifier(serializedFirstComponent, into: &bytes) + + while let component = components.popFirst() { + ASN1ObjectIdentifier.writeOIDSubidentifier(component, into: &bytes) + } + } + } + + private static func writeOIDSubidentifier(_ identifier: UInt, into array: inout [UInt8]) { + // An OID subidentifier is written as an integer over 7-bit bytes, where the last byte has the top bit unset. + // The first thing we need is to know how many bits we need to write + let bitsToWrite = UInt.bitWidth - identifier.leadingZeroBitCount + let bytesToWrite = (bitsToWrite + 6) / 7 + + guard bytesToWrite > 0 else { + // Just a zero. + array.append(0) + return + } + + for byteNumber in (1..> shift) & 0x7f) | 0x80 + array.append(byte) + } + + // Last byte to append here, we must unset the top bit. + let byte = UInt8((identifier & 0x7F)) + array.append(byte) + } + } +} + +extension ASN1.ASN1ObjectIdentifier: Hashable {} + +extension ASN1.ASN1ObjectIdentifier: ExpressibleByArrayLiteral { + init(arrayLiteral elements: UInt...) { + self.oidComponents = elements + } + } + +extension ASN1.ASN1ObjectIdentifier { + enum NamedCurves { + + /// ASN1 identifier for `secp256k1` + /// https://oidref.com/1.3.132.0.10 + static let secp256k1: ASN1.ASN1ObjectIdentifier = [1, 3, 132, 0, 10] + + } + + enum HashFunctions { + static let sha256: ASN1.ASN1ObjectIdentifier = [2, 16, 840, 1, 101, 3, 4, 2, 1] + static let sha384: ASN1.ASN1ObjectIdentifier = [2, 16, 840, 1, 101, 3, 4, 2, 2] + static let sha512: ASN1.ASN1ObjectIdentifier = [2, 16, 840, 1, 101, 3, 4, 2, 3] + } + + enum AlgorithmIdentifier { + static let idEcPublicKey: ASN1.ASN1ObjectIdentifier = [1, 2, 840, 10_045, 2, 1] + } + + enum NameAttributes { + static let name: ASN1.ASN1ObjectIdentifier = [2, 5, 4, 41] + static let surname: ASN1.ASN1ObjectIdentifier = [2, 5, 4, 4] + static let givenName: ASN1.ASN1ObjectIdentifier = [2, 5, 4, 42] + static let initials: ASN1.ASN1ObjectIdentifier = [2, 5, 4, 43] + static let generationQualifier: ASN1.ASN1ObjectIdentifier = [2, 5, 4, 44] + static let commonName: ASN1.ASN1ObjectIdentifier = [2, 5, 4, 3] + static let localityName: ASN1.ASN1ObjectIdentifier = [2, 5, 4, 7] + static let stateOrProvinceName: ASN1.ASN1ObjectIdentifier = [2, 5, 4, 8] + static let organizationName: ASN1.ASN1ObjectIdentifier = [2, 5, 4, 10] + static let organizationalUnitName: ASN1.ASN1ObjectIdentifier = [2, 5, 4, 11] + static let title: ASN1.ASN1ObjectIdentifier = [2, 5, 4, 12] + static let dnQualifier: ASN1.ASN1ObjectIdentifier = [2, 5, 4, 46] + static let countryName: ASN1.ASN1ObjectIdentifier = [2, 5, 4, 6] + static let serialNumber: ASN1.ASN1ObjectIdentifier = [2, 5, 4, 5] + static let pseudonym: ASN1.ASN1ObjectIdentifier = [2, 5, 4, 65] + static let domainComponent: ASN1.ASN1ObjectIdentifier = [0, 9, 2342, 19_200_300, 100, 1, 25] + static let emailAddress: ASN1.ASN1ObjectIdentifier = [1, 2, 840, 113_549, 1, 9, 1] + } + +} + +extension ArraySlice where Element == UInt8 { + mutating fileprivate func readOIDSubidentifier() throws -> UInt { + // In principle OID subidentifiers can be too large to fit into a UInt. We are choosing to not care about that + // because for us it shouldn't matter. + guard let subidentifierEndIndex = self.firstIndex(where: { $0 & 0x80 == 0x00 }) else { + throw CryptoKitASN1Error.invalidASN1Object + } + + let oidSlice = self[self.startIndex ... subidentifierEndIndex] + self = self[self.index(after: subidentifierEndIndex)...] + + // We need to compact the bits. These are 7-bit integers, which is really awkward. + return try UInt(sevenBitBigEndianBytes: oidSlice) + } +} + +extension UInt { + fileprivate init(sevenBitBigEndianBytes bytes: Bytes) throws where Bytes.Element == UInt8 { + // We need to know how many bytes we _need_ to store this "int". + guard ((bytes.count * 7) + 7) / 8 <= MemoryLayout.size else { + throw CryptoKitASN1Error.invalidASN1Object + } + + self = 0 + let shiftSizes = stride(from: 0, to: bytes.count * 7, by: 7).reversed() + + var index = bytes.startIndex + for shift in shiftSizes { + self |= UInt(bytes[index] & 0x7F) << shift + bytes.formIndex(after: &index) + } + } +} + +#endif // Linux or !SwiftPM diff --git a/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/ECDSASignature.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/ECDSASignature.swift new file mode 100644 index 0000000..a89f40d --- /dev/null +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/ECDSASignature.swift @@ -0,0 +1,59 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +#if (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API +@_exported import CryptoKit +#else +import Foundation + +extension ASN1 { + /// An ECDSA signature is laid out as follows: + /// + /// ECDSASignature ::= SEQUENCE { + /// r INTEGER, + /// s INTEGER + /// } + /// + /// This type is generic because our different backends want to use different bignum representations. + struct ECDSASignature: ASN1ImplicitlyTaggable { + static var defaultIdentifier: ASN1.ASN1Identifier { + .sequence + } + + var r: IntegerType + var s: IntegerType + + init(r: IntegerType, s: IntegerType) { + self.r = r + self.s = s + } + + init(asn1Encoded rootNode: ASN1.ASN1Node, withIdentifier identifier: ASN1.ASN1Identifier) throws { + self = try ASN1.sequence(rootNode, identifier: identifier) { nodes in + let r = try IntegerType(asn1Encoded: &nodes) + let s = try IntegerType(asn1Encoded: &nodes) + + return ECDSASignature(r: r, s: s) + } + } + + func serialize(into coder: inout ASN1.Serializer, withIdentifier identifier: ASN1.ASN1Identifier) throws { + try coder.appendConstructedNode(identifier: identifier) { coder in + try coder.serialize(self.r) + try coder.serialize(self.s) + } + } + } +} + +#endif // Linux or !SwiftPM diff --git a/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/PEMDocument.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/PEMDocument.swift new file mode 100644 index 0000000..f2f8d7e --- /dev/null +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/PEMDocument.swift @@ -0,0 +1,118 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +#if (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API +@_exported import CryptoKit +#else +import Foundation + +extension ASN1 { + /// A PEM document is some data, and a discriminator type that is used to advertise the content. + struct PEMDocument { + private static let lineLength = 64 + + var type: String + + var derBytes: Data + + init(pemString: String) throws { + // A PEM document looks like this: + // + // -----BEGIN ----- + // + // -----END ----- + // + // This function attempts to parse this string as a PEM document, and returns the discriminator type + // and the base64 decoded bytes. + var lines = pemString.split { $0.isNewline }[...] + guard let first = lines.first, let last = lines.last else { + throw CryptoKitASN1Error.invalidPEMDocument + } + + guard let discriminator = first.pemStartDiscriminator, discriminator == last.pemEndDiscriminator else { + throw CryptoKitASN1Error.invalidPEMDocument + } + + // All but the last line must be 64 bytes. The force unwrap is safe because we require the lines to be + // greater than zero. + lines = lines.dropFirst().dropLast() + guard lines.count > 0, + lines.dropLast().allSatisfy({ $0.utf8.count == PEMDocument.lineLength }), + lines.last!.utf8.count <= PEMDocument.lineLength else { + throw CryptoKitASN1Error.invalidPEMDocument + } + + guard let derBytes = Data(base64Encoded: lines.joined()) else { + throw CryptoKitASN1Error.invalidPEMDocument + } + + self.type = discriminator + self.derBytes = derBytes + } + + init(type: String, derBytes: Data) { + self.type = type + self.derBytes = derBytes + } + + var pemString: String { + var encoded = self.derBytes.base64EncodedString()[...] + let pemLineCount = (encoded.utf8.count + PEMDocument.lineLength) / PEMDocument.lineLength + var pemLines = [Substring]() + pemLines.reserveCapacity(pemLineCount + 2) + + pemLines.append("-----BEGIN \(self.type)-----") + + while encoded.count > 0 { + let prefixIndex = encoded.index(encoded.startIndex, offsetBy: PEMDocument.lineLength, limitedBy: encoded.endIndex) ?? encoded.endIndex + pemLines.append(encoded[.. String? { + var utf8Bytes = self.utf8[...] + + // We want to split this sequence into three parts: the prefix, the middle, and the end + let prefixSize = expectedPrefix.utf8.count + let suffixSize = expectedSuffix.utf8.count + + let prefix = utf8Bytes.prefix(prefixSize) + utf8Bytes = utf8Bytes.dropFirst(prefixSize) + let suffix = utf8Bytes.suffix(suffixSize) + utf8Bytes = utf8Bytes.dropLast(suffixSize) + + guard prefix.elementsEqual(expectedPrefix.utf8), suffix.elementsEqual(expectedSuffix.utf8) else { + return nil + } + + return String(utf8Bytes) + } +} + +#endif // Linux or !SwiftPM diff --git a/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/PKCS8PrivateKey.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/PKCS8PrivateKey.swift new file mode 100644 index 0000000..c35f762 --- /dev/null +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/PKCS8PrivateKey.swift @@ -0,0 +1,103 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +#if (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API +@_exported import CryptoKit +#else +import Foundation + +extension ASN1 { + // A PKCS#8 private key is one of two formats, depending on the version: + // + // For PKCS#8 we need the following for the private key: + // + // PrivateKeyInfo ::= SEQUENCE { + // version Version, + // privateKeyAlgorithm PrivateKeyAlgorithmIdentifier, + // privateKey PrivateKey, + // attributes [0] IMPLICIT Attributes OPTIONAL } + // + // Version ::= INTEGER + // + // PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier + // + // PrivateKey ::= OCTET STRING + // + // Attributes ::= SET OF Attribute + // + // We disregard the attributes because we don't support them anyway. + // + // The private key octet string contains (surprise!) a SEC1-encoded private key! So we recursively invoke the + // ASN.1 parser and go again. + struct PKCS8PrivateKey: ASN1ImplicitlyTaggable { + static var defaultIdentifier: ASN1.ASN1Identifier { + return .sequence + } + + var algorithm: RFC5480AlgorithmIdentifier + + var privateKey: ASN1.SEC1PrivateKey + + init(asn1Encoded rootNode: ASN1.ASN1Node, withIdentifier identifier: ASN1.ASN1Identifier) throws { + self = try ASN1.sequence(rootNode, identifier: identifier) { nodes in + let version = try Int(asn1Encoded: &nodes) + guard version == 0 else { + throw CryptoKitASN1Error.invalidASN1Object + } + + let algorithm = try ASN1.RFC5480AlgorithmIdentifier(asn1Encoded: &nodes) + let privateKeyBytes = try ASN1.ASN1OctetString(asn1Encoded: &nodes) + + // We ignore the attributes + _ = try ASN1.optionalExplicitlyTagged(&nodes, tagNumber: 0, tagClass: .contextSpecific) { _ in } + + let sec1PrivateKeyNode = try ASN1.parse(privateKeyBytes.bytes) + let sec1PrivateKey = try ASN1.SEC1PrivateKey(asn1Encoded: sec1PrivateKeyNode) + if let innerAlgorithm = sec1PrivateKey.algorithm, innerAlgorithm != algorithm { + throw CryptoKitASN1Error.invalidASN1Object + } + + return try .init(algorithm: algorithm, privateKey: sec1PrivateKey) + } + } + + private init(algorithm: ASN1.RFC5480AlgorithmIdentifier, privateKey: ASN1.SEC1PrivateKey) throws { + self.privateKey = privateKey + self.algorithm = algorithm + } + + init(algorithm: ASN1.RFC5480AlgorithmIdentifier, privateKey: [UInt8], publicKey: [UInt8]) { + self.algorithm = algorithm + + // We nil out the private key here. I don't really know why we do this, but OpenSSL does, and it seems + // safe enough to do: it certainly avoids the possibility of disagreeing on what it is! + self.privateKey = ASN1.SEC1PrivateKey(privateKey: privateKey, algorithm: nil, publicKey: publicKey) + } + + func serialize(into coder: inout ASN1.Serializer, withIdentifier identifier: ASN1.ASN1Identifier) throws { + try coder.appendConstructedNode(identifier: identifier) { coder in + try coder.serialize(0) // version + try coder.serialize(self.algorithm) + + // Here's a weird one: we recursively serialize the private key, and then turn the bytes into an octet string. + var subCoder = ASN1.Serializer() + try subCoder.serialize(self.privateKey) + let serializedKey = ASN1.ASN1OctetString(contentBytes: subCoder.serializedBytes[...]) + + try coder.serialize(serializedKey) + } + } + } +} + +#endif // Linux or !SwiftPM diff --git a/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/SEC1PrivateKey.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/SEC1PrivateKey.swift new file mode 100644 index 0000000..5cc1b76 --- /dev/null +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/SEC1PrivateKey.swift @@ -0,0 +1,108 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Changes by Alexander Cyon: +// * Removed support for swift-crypto curves +// * Added support for curve `secp256k1` + + +#if (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API +@_exported import CryptoKit +#else +import Foundation + +extension ASN1 { + // For private keys, SEC 1 uses: + // + // ECPrivateKey ::= SEQUENCE { + // version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), + // privateKey OCTET STRING, + // parameters [0] EXPLICIT ECDomainParameters OPTIONAL, + // publicKey [1] EXPLICIT BIT STRING OPTIONAL + // } + struct SEC1PrivateKey: ASN1ImplicitlyTaggable { + static var defaultIdentifier: ASN1.ASN1Identifier { + return .sequence + } + + var algorithm: ASN1.RFC5480AlgorithmIdentifier? + + var privateKey: ASN1.ASN1OctetString + + var publicKey: ASN1.ASN1BitString? + + init(asn1Encoded rootNode: ASN1.ASN1Node, withIdentifier identifier: ASN1.ASN1Identifier) throws { + self = try ASN1.sequence(rootNode, identifier: identifier) { nodes in + let version = try Int(asn1Encoded: &nodes) + guard 1 == version else { + throw CryptoKitASN1Error.invalidASN1Object + } + + let privateKey = try ASN1OctetString(asn1Encoded: &nodes) + let parameters = try ASN1.optionalExplicitlyTagged(&nodes, tagNumber: 0, tagClass: .contextSpecific) { node in + return try ASN1.ASN1ObjectIdentifier(asn1Encoded: node) + } + let publicKey = try ASN1.optionalExplicitlyTagged(&nodes, tagNumber: 1, tagClass: .contextSpecific) { node in + return try ASN1.ASN1BitString(asn1Encoded: node) + } + + return try .init(privateKey: privateKey, algorithm: parameters, publicKey: publicKey) + } + } + + private init(privateKey: ASN1.ASN1OctetString, algorithm: ASN1.ASN1ObjectIdentifier?, publicKey: ASN1.ASN1BitString?) throws { + self.privateKey = privateKey + self.publicKey = publicKey + self.algorithm = try algorithm.map { algorithmOID in + switch algorithmOID { + case ASN1ObjectIdentifier.NamedCurves.secp256k1: + return .secp256k1 + default: + throw CryptoKitASN1Error.invalidASN1Object + } + } + } + + init(privateKey: [UInt8], algorithm: RFC5480AlgorithmIdentifier?, publicKey: [UInt8]) { + self.privateKey = ASN1OctetString(contentBytes: privateKey[...]) + self.algorithm = algorithm + self.publicKey = ASN1BitString(bytes: publicKey[...]) + } + + func serialize(into coder: inout ASN1.Serializer, withIdentifier identifier: ASN1.ASN1Identifier) throws { + try coder.appendConstructedNode(identifier: identifier) { coder in + try coder.serialize(1) // version + try coder.serialize(self.privateKey) + + if let algorithm = self.algorithm { + let oid: ASN1.ASN1ObjectIdentifier + switch algorithm { + case .secp256k1: + oid = ASN1ObjectIdentifier.NamedCurves.secp256k1 + default: + throw CryptoKitASN1Error.invalidASN1Object + } + + try coder.serialize(oid, explicitlyTaggedWithTagNumber: 0, tagClass: .contextSpecific) + } + + if let publicKey = self.publicKey { + try coder.serialize(publicKey, explicitlyTaggedWithTagNumber: 1, tagClass: .contextSpecific) + } + } + } + } +} + +#endif // Linux or !SwiftPM diff --git a/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/SubjectPublicKeyInfo.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/SubjectPublicKeyInfo.swift new file mode 100644 index 0000000..f87f396 --- /dev/null +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1/SubjectPublicKeyInfo.swift @@ -0,0 +1,125 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Changes by Alexander Cyon: +// * Removed RFC5480AlgorithmIdentifier for swift-crypto curves +// * Added RFC5480AlgorithmIdentifier for curve `secp256k1` + + +#if (os(macOS) || os(iOS) || os(watchOS) || os(tvOS)) && CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API +@_exported import CryptoKit +#else +import Foundation + +extension ASN1 { + struct SubjectPublicKeyInfo: ASN1ImplicitlyTaggable { + static var defaultIdentifier: ASN1.ASN1Identifier { + .sequence + } + + var algorithmIdentifier: RFC5480AlgorithmIdentifier + + var key: ASN1.ASN1BitString + + init(asn1Encoded rootNode: ASN1.ASN1Node, withIdentifier identifier: ASN1.ASN1Identifier) throws { + // The SPKI block looks like this: + // + // SubjectPublicKeyInfo ::= SEQUENCE { + // algorithm AlgorithmIdentifier, + // subjectPublicKey BIT STRING + // } + self = try ASN1.sequence(rootNode, identifier: identifier) { nodes in + let algorithmIdentifier = try ASN1.RFC5480AlgorithmIdentifier(asn1Encoded: &nodes) + let key = try ASN1.ASN1BitString(asn1Encoded: &nodes) + + return SubjectPublicKeyInfo(algorithmIdentifier: algorithmIdentifier, key: key) + } + } + + private init(algorithmIdentifier: RFC5480AlgorithmIdentifier, key: ASN1.ASN1BitString) { + self.algorithmIdentifier = algorithmIdentifier + self.key = key + } + + internal init(algorithmIdentifier: RFC5480AlgorithmIdentifier, key: [UInt8]) { + self.algorithmIdentifier = algorithmIdentifier + self.key = ASN1BitString(bytes: key[...]) + } + + func serialize(into coder: inout ASN1.Serializer, withIdentifier identifier: ASN1.ASN1Identifier) throws { + try coder.appendConstructedNode(identifier: identifier) { coder in + try coder.serialize(self.algorithmIdentifier) + try coder.serialize(self.key) + } + } + } + + struct RFC5480AlgorithmIdentifier: ASN1ImplicitlyTaggable, Hashable { + static var defaultIdentifier: ASN1.ASN1Identifier { + .sequence + } + + var algorithm: ASN1.ASN1ObjectIdentifier + + var parameters: ASN1.ASN1Any? + + init(algorithm: ASN1.ASN1ObjectIdentifier, parameters: ASN1.ASN1Any?) { + self.algorithm = algorithm + self.parameters = parameters + } + + init(asn1Encoded rootNode: ASN1.ASN1Node, withIdentifier identifier: ASN1.ASN1Identifier) throws { + // The AlgorithmIdentifier block looks like this. + // + // AlgorithmIdentifier ::= SEQUENCE { + // algorithm OBJECT IDENTIFIER, + // parameters ANY DEFINED BY algorithm OPTIONAL + // } + // + // ECParameters ::= CHOICE { + // namedCurve OBJECT IDENTIFIER + // -- implicitCurve NULL + // -- specifiedCurve SpecifiedECDomain + // } + // + // We don't bother with helpers: we just try to decode it directly. + self = try ASN1.sequence(rootNode, identifier: identifier) { nodes in + let algorithmOID = try ASN1.ASN1ObjectIdentifier(asn1Encoded: &nodes) + + let parameters = nodes.next().map { ASN1.ASN1Any(asn1Encoded: $0) } + + return .init(algorithm: algorithmOID, parameters: parameters) + } + } + + func serialize(into coder: inout ASN1.Serializer, withIdentifier identifier: ASN1.ASN1Identifier) throws { + try coder.appendConstructedNode(identifier: identifier) { coder in + try coder.serialize(self.algorithm) + if let parameters = self.parameters { + try coder.serialize(parameters) + } + } + } + } +} + +// MARK: Algorithm Identifier Statics +extension ASN1.RFC5480AlgorithmIdentifier { + static let secp256k1 = ASN1.RFC5480AlgorithmIdentifier( + algorithm: .AlgorithmIdentifier.idEcPublicKey, + parameters: try! .init(erasing: ASN1.ASN1ObjectIdentifier.NamedCurves.secp256k1) + ) +} + +#endif // Linux or !SwiftPM diff --git a/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1Error.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1Error.swift new file mode 100644 index 0000000..9c258e4 --- /dev/null +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/ASN1Error.swift @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.md for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// FROM: https://github.com/apple/swift-crypto/blob/5d8b14d22701c8394ad8cd5b297f8ffd9a6d3d4a/Sources/Crypto/CryptoKitErrors.swift#L34 + +/// Errors encountered when parsing ASN.1 formatted keys. +public enum CryptoKitASN1Error: Error { + /// The ASN.1 tag for this field is invalid or unsupported. + case invalidFieldIdentifier + + /// The ASN.1 tag for the parsed field does not match the required format. + case unexpectedFieldType + + /// An invalid ASN.1 object identifier was encountered. + case invalidObjectIdentifier + + /// The format of the parsed ASN.1 object does not match the format required for the data type + /// being decoded. + case invalidASN1Object + + /// An ASN.1 integer was decoded that does not use the minimum number of bytes for its encoding. + case invalidASN1IntegerEncoding + + /// An ASN.1 field was truncated and could not be decoded. + case truncatedASN1Field + + /// The encoding used for the field length is not supported. + case unsupportedFieldLength + + /// It was not possible to parse a string as a PEM document. + case invalidPEMDocument +} diff --git a/Sources/K1/ThirdyParty/swift-crypto/BytesUtil.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/BytesUtil.swift similarity index 96% rename from Sources/K1/ThirdyParty/swift-crypto/BytesUtil.swift rename to Sources/K1/Support/ThirdyParty/swift-crypto/BytesUtil.swift index 46fa4ed..a8e6d9c 100644 --- a/Sources/K1/ThirdyParty/swift-crypto/BytesUtil.swift +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/BytesUtil.swift @@ -52,17 +52,17 @@ extension Array where Element == UInt8 { guard hex.count.isMultiple(of: 2), !hex.isEmpty else { throw ByteHexEncodingErrors.incorrectString } - + let stringBytes: [UInt8] = Array(hex.data(using: String.Encoding.utf8)!) - + for i in 0...((hex.count / 2) - 1) { let char1 = stringBytes[2 * i] let char2 = stringBytes[2 * i + 1] - + try self.append(htoi(char1) << 4 + htoi(char2)) } } - + } extension DataProtocol { @@ -71,7 +71,7 @@ extension DataProtocol { let hexLen = self.count * 2 let ptr = UnsafeMutablePointer.allocate(capacity: hexLen) var offset = 0 - + self.regions.forEach { (_) in for i in self { ptr[Int(offset * 2)] = itoh((i >> 4) & 0xF) @@ -79,7 +79,7 @@ extension DataProtocol { offset += 1 } } - + return String(bytesNoCopy: ptr, length: hexLen, encoding: .utf8, freeWhenDone: true)! } } @@ -88,19 +88,19 @@ extension DataProtocol { extension Data { init(hex: String) throws { self.init() - + if hex.count % 2 != 0 || hex.count == 0 { throw ByteHexEncodingErrors.incorrectString } - + let stringBytes: [UInt8] = Array(hex.lowercased().data(using: String.Encoding.utf8)!) - + for i in 0...((hex.count / 2) - 1) { let char1 = stringBytes[2 * i] let char2 = stringBytes[2 * i + 1] - + try self.append(htoi(char1) << 4 + htoi(char2)) } } - + } diff --git a/Sources/K1/ThirdyParty/swift-crypto/RNG_boring.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/RNG_boring.swift similarity index 100% rename from Sources/K1/ThirdyParty/swift-crypto/RNG_boring.swift rename to Sources/K1/Support/ThirdyParty/swift-crypto/RNG_boring.swift diff --git a/Sources/K1/ThirdyParty/swift-crypto/SafeCompare_boring.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/SafeCompare_boring.swift similarity index 92% rename from Sources/K1/ThirdyParty/swift-crypto/SafeCompare_boring.swift rename to Sources/K1/Support/ThirdyParty/swift-crypto/SafeCompare_boring.swift index 60c6221..cc85fe5 100644 --- a/Sources/K1/ThirdyParty/swift-crypto/SafeCompare_boring.swift +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/SafeCompare_boring.swift @@ -20,7 +20,7 @@ import Foundation /// This function performs a safe comparison between two buffers of bytes. It exists as a temporary shim until we refactor /// some of the usage sites to pass better data structures to us. @inlinable -internal func openSSLSafeCompare(_ lhs: LHS, _ rhs: RHS) -> Bool { +func safeCompare(_ lhs: LHS, _ rhs: RHS) -> Bool { lhs.withUnsafeBytes { lhsPtr in rhs.withUnsafeBytes { rhsPtr in constantTimeCompare(lhsPtr, rhsPtr) diff --git a/Sources/K1/ThirdyParty/swift-crypto/SecureBytes.swift b/Sources/K1/Support/ThirdyParty/swift-crypto/SecureBytes.swift similarity index 99% rename from Sources/K1/ThirdyParty/swift-crypto/SecureBytes.swift rename to Sources/K1/Support/ThirdyParty/swift-crypto/SecureBytes.swift index d5da6f3..1c3dace 100644 --- a/Sources/K1/ThirdyParty/swift-crypto/SecureBytes.swift +++ b/Sources/K1/Support/ThirdyParty/swift-crypto/SecureBytes.swift @@ -76,7 +76,7 @@ extension SecureBytes { // MARK: - Equatable conformance, constant-time extension SecureBytes: Equatable { - public static func == (lhs: SecureBytes, rhs: SecureBytes) -> Bool { + static func == (lhs: SecureBytes, rhs: SecureBytes) -> Bool { return safeCompare(lhs, rhs) } } diff --git a/Sources/K1/ThirdyParty/swift-crypto/SafeCompare.swift b/Sources/K1/ThirdyParty/swift-crypto/SafeCompare.swift deleted file mode 100644 index afd22c5..0000000 --- a/Sources/K1/ThirdyParty/swift-crypto/SafeCompare.swift +++ /dev/null @@ -1,24 +0,0 @@ -// from: https://github.com/apple/swift-crypto/blob/794901c991bf3fa0431ba3c0927ba078799c6911/Sources/Crypto/Util/SafeCompare.swift -// commit: 794901c991bf3fa0431ba3c0927ba078799c6911 -// editing done in compliance with Apache License - -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftCrypto open source project -// -// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.md for the list of SwiftCrypto project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Foundation - -internal func safeCompare(_ lhs: LHS, _ rhs: RHS) -> Bool { - return openSSLSafeCompare(lhs, rhs) -} - diff --git a/Sources/K1/Util/MemoizationBox.swift b/Sources/K1/Util/MemoizationBox.swift deleted file mode 100644 index cb7f572..0000000 --- a/Sources/K1/Util/MemoizationBox.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-28. -// - -import Foundation - -// https://forums.swift.org/t/proposal-property-behaviors/594 -internal final class MemoizationBox { - private let lock = NSLock() - private var value: T? = nil - - internal init() {} - - func getOrEvaluate(fn: () -> T) -> T { - if let value = value { return value } - // Perform initialization in a thread-safe way. - return lock.with { - let initialValue = fn() - value = initialValue - return initialValue - } - } -} - -extension NSLock { - - @discardableResult - func with(_ block: () throws -> T) rethrows -> T { - lock() - defer { unlock() } - return try block() - } -} diff --git a/Sources/secp256k1/include/secp256k1.h b/Sources/secp256k1/include/secp256k1.h index 6c2d66c..1d6d15a 100644 --- a/Sources/secp256k1/include/secp256k1.h +++ b/Sources/secp256k1/include/secp256k1.h @@ -5,4 +5,4 @@ #include "../libsecp256k1/include/secp256k1_schnorrsig.h" #include "../libsecp256k1/include/secp256k1.h" -#include "../src/ecdh_no_hashfp.h" +#include "../src/ecdh_variants.h" diff --git a/Sources/secp256k1/src/ecdh_no_hashfp.h b/Sources/secp256k1/src/ecdh_no_hashfp.h deleted file mode 100644 index 7c95bbc..0000000 --- a/Sources/secp256k1/src/ecdh_no_hashfp.h +++ /dev/null @@ -1,21 +0,0 @@ -// -// ecdh_no_hashfp.h -// -// -// Created by Alexander Cyon on 2022-01-31. -// - -#ifndef ecdh_no_hashfp_h -#define ecdh_no_hashfp_h - -#include "../libsecp256k1/include/secp256k1_ecdh.h" - -int ecdh_skip_hash_extract_x_and_y(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data); - - -int ecdh_skip_hash_extract_only_x(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data); - - - - -#endif /* ecdh_no_hashfp_h */ diff --git a/Sources/secp256k1/src/ecdh_no_hashfp.c b/Sources/secp256k1/src/ecdh_variants.c similarity index 51% rename from Sources/secp256k1/src/ecdh_no_hashfp.c rename to Sources/secp256k1/src/ecdh_variants.c index 8a2863e..6b4df1e 100644 --- a/Sources/secp256k1/src/ecdh_no_hashfp.c +++ b/Sources/secp256k1/src/ecdh_variants.c @@ -1,14 +1,14 @@ // -// ecdh_no_hashfp.c +// ecdh_variants.c // // // Created by Alexander Cyon on 2022-01-31. // -#include "./ecdh_no_hashfp.h" +#include "./ecdh_variants.h" #include -int ecdh_skip_hash_extract_x_and_y(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data) { +int ecdh_unsafe_whole_point(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data) { (void)data; /* Save x and y as uncompressed public key */ output[0] = 0x04; @@ -17,7 +17,7 @@ int ecdh_skip_hash_extract_x_and_y(unsigned char *output, const unsigned char *x return 1; } -int ecdh_skip_hash_extract_only_x(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data) { +int ecdh_asn1_x963(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data) { (void)data; memcpy(output, x32, 32); return 1; diff --git a/Sources/secp256k1/src/ecdh_variants.h b/Sources/secp256k1/src/ecdh_variants.h new file mode 100644 index 0000000..32c5d26 --- /dev/null +++ b/Sources/secp256k1/src/ecdh_variants.h @@ -0,0 +1,39 @@ +// +// ecdh_variants.h +// +// +// Created by Alexander Cyon on 2022-01-31. +// + +#ifndef ecdh_variants_h +#define ecdh_variants_h + +#include "../libsecp256k1/include/secp256k1_ecdh.h" + +/** An unsafe ECDH variant which returns the whole ECDH secret (uncompressed point, unhashed) + * + * Returns: 1 if the point was successfully hashed. + * 0 will cause secp256k1_ecdh to fail and return 0. + * Other return values are not allowed, and the behaviour of + * secp256k1_ecdh is undefined for other return values. + * Out: output: pointer to an array to be filled by the function + * In: x32: pointer to a 32-byte x coordinate + * y32: pointer to a 32-byte y coordinate + * data: arbitrary data pointer that is passed through + */ +int ecdh_unsafe_whole_point(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data); + +/** The ASN1 X9.63 ECDH variant which returns only the X component of ECDH secret (unhashed). + * + * Returns: 1 if the point was successfully hashed. + * 0 will cause secp256k1_ecdh to fail and return 0. + * Other return values are not allowed, and the behaviour of + * secp256k1_ecdh is undefined for other return values. + * Out: output: pointer to an array to be filled by the function + * In: x32: pointer to a 32-byte x coordinate + * y32: pointer to a 32-byte y coordinate + * data: arbitrary data pointer that is passed through + */ +int ecdh_asn1_x963(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data); + +#endif /* ecdh_variants_h */ diff --git a/Tests/K1Tests/TestCases/ECDH/ECDHTests.swift b/Tests/K1Tests/TestCases/ECDH/ECDHTests.swift index 7f47e51..145e796 100644 --- a/Tests/K1Tests/TestCases/ECDH/ECDHTests.swift +++ b/Tests/K1Tests/TestCases/ECDH/ECDHTests.swift @@ -12,9 +12,9 @@ import XCTest final class ECDHTests: XCTestCase { func testECDHX963() throws { - let alice = try K1.PrivateKey.generateNew() - let bob = try K1.PrivateKey.generateNew() - + let alice = K1.KeyAgreement.PrivateKey() + let bob = K1.KeyAgreement.PrivateKey() + XCTAssertNotEqual(alice, bob) let ab = try alice.sharedSecretFromKeyAgreement(with: bob.publicKey) let ba = try bob.sharedSecretFromKeyAgreement(with: alice.publicKey) ab.withUnsafeBytes { @@ -24,19 +24,21 @@ final class ECDHTests: XCTestCase { } func testECDHLibsecp256k1() throws { - let alice = try K1.PrivateKey.generateNew() - let bob = try K1.PrivateKey.generateNew() - + let alice = K1.KeyAgreement.PrivateKey() + let bob = K1.KeyAgreement.PrivateKey() + XCTAssertNotEqual(alice, bob) let ab = try alice.ecdh(with: bob.publicKey) let ba = try bob.ecdh(with: alice.publicKey) + ab.withUnsafeBytes { + XCTAssertEqual(Data($0).count, 32) + } XCTAssertEqual(ab, ba, "Alice and Bob should be able to agree on the same secret") - XCTAssertEqual(ab.count, 32) } func testECDHPoint() throws { - let alice = try K1.PrivateKey.generateNew() - let bob = try K1.PrivateKey.generateNew() - + let alice = K1.KeyAgreement.PrivateKey() + let bob = K1.KeyAgreement.PrivateKey() + XCTAssertNotEqual(alice, bob) let ab = try alice.ecdhPoint(with: bob.publicKey) let ba = try bob.ecdhPoint(with: alice.publicKey) XCTAssertEqual(ab, ba, "Alice and Bob should be able to agree on the same secret") @@ -45,21 +47,24 @@ final class ECDHTests: XCTestCase { /// Test vectors from: https://crypto.stackexchange.com/q/57695 func test_crypto_stackexchange_vector() throws { - let privateKey1 = try PrivateKey(hex: "82fc9947e878fc7ed01c6c310688603f0a41c8e8704e5b990e8388343b0fd465") - let privateKey2 = try PrivateKey(hex: "5f706787ac72c1080275c1f398640fb07e9da0b124ae9734b28b8d0f01eda586") + let privateKey1 = try K1.KeyAgreement.PrivateKey(hex: "82fc9947e878fc7ed01c6c310688603f0a41c8e8704e5b990e8388343b0fd465") + let privateKey2 = try K1.KeyAgreement.PrivateKey(hex: "5f706787ac72c1080275c1f398640fb07e9da0b124ae9734b28b8d0f01eda586") let libsecp256k1 = try privateKey1.ecdh(with: privateKey2.publicKey) let ans1X963 = try privateKey1.sharedSecretFromKeyAgreement(with: privateKey2.publicKey).withUnsafeBytes({ Data($0) }) - XCTAssertEqual(libsecp256k1.hex, "5935d0476af9df2998efb60383adf2ff23bc928322cfbb738fca88e49d557d7e") + libsecp256k1.withUnsafeBytes { + XCTAssertEqual(Data($0).hex, "5935d0476af9df2998efb60383adf2ff23bc928322cfbb738fca88e49d557d7e") + } + XCTAssertEqual(ans1X963.hex, "3a17fe5fa33c4f2c7e61799a65061214913f39bfcbee178ab351493d5ee17b2f") } /// Assert we do not introduce any regression bugs for the custom ECDh `ecdhPoint` func testECDHCustom() throws { - let alice = try K1.PrivateKey.import(rawRepresentation: Data(repeating: 0xAA, count: 32)) - let bob = try K1.PrivateKey.import(rawRepresentation: Data(repeating: 0xBB, count: 32)) + let alice = try K1.KeyAgreement.PrivateKey(rawRepresentation: Data(repeating: 0xAA, count: 32)) + let bob = try K1.KeyAgreement.PrivateKey(rawRepresentation: Data(repeating: 0xBB, count: 32)) let ab = try alice.ecdhPoint(with: bob.publicKey) let ba = try bob.ecdhPoint(with: alice.publicKey) XCTAssertEqual(ab, ba, "Alice and Bob should be able to agree on the same secret") @@ -68,8 +73,8 @@ final class ECDHTests: XCTestCase { } } -extension K1.PrivateKey { +extension K1.KeyAgreement.PrivateKey { init(hex: String) throws { - self = try Self.import(rawRepresentation: Data(hex: hex)) + self = try Self(rawRepresentation: Data(hex: hex)) } } diff --git a/Tests/K1Tests/TestCases/ECDH/ECDHWychoproofTests.swift b/Tests/K1Tests/TestCases/ECDH/ECDHWychoproofTests.swift index c159801..0698e03 100644 --- a/Tests/K1Tests/TestCases/ECDH/ECDHWychoproofTests.swift +++ b/Tests/K1Tests/TestCases/ECDH/ECDHWychoproofTests.swift @@ -22,7 +22,7 @@ final class ECDHWychoproofTests: XCTestCase { func testECDHWycheproof() throws { let result = try testSuite( - jsonName: "ecdh_secp256k1_test", + jsonName: "wycheproof_ecdh_ASN1x963", testFunction: { (group: ECDHTestGroup) in testGroup(group: group) }) @@ -45,7 +45,7 @@ private extension ECDHWychoproofTests { // // Second, Wycheproof inputs may be too short or too long with // leading zeros. - let curveSize = K1.Curve.Field.byteCount + let curveSize = 32 //K1.Curve.Field.byteCount var privateBytes = [UInt8](repeating: 0, count: curveSize) let hexFromVector = (vector.count % 2 == 0) ? vector : "0\(vector)" @@ -74,10 +74,10 @@ private extension ECDHWychoproofTests { } numberOfTestsRun += 1 do { - let publicKey = try PublicKey.import(der: Data(hex: testVector.publicKey)) + let publicKey = try K1.KeyAgreement.PublicKey(derRepresentation: Data(hex: testVector.publicKey)) var privateBytes = [UInt8]() privateBytes = try padKeyIfNecessary(vector: testVector.privateKey) - let privateKey = try PrivateKey.import(rawRepresentation: privateBytes) + let privateKey = try K1.KeyAgreement.PrivateKey(rawRepresentation: privateBytes) /// ANS1 X9.63 serialization of shared secret, returning a `CryptoKit.SharedSecret` let sharedPublicKeyPoint = try privateKey.sharedSecretFromKeyAgreement(with: publicKey) diff --git a/Tests/K1Tests/TestCases/ECDH/TwoVariantsOfECDHWithKDFTests.swift b/Tests/K1Tests/TestCases/ECDH/TwoVariantsOfECDHWithKDFTests.swift index 44dd647..af223b0 100644 --- a/Tests/K1Tests/TestCases/ECDH/TwoVariantsOfECDHWithKDFTests.swift +++ b/Tests/K1Tests/TestCases/ECDH/TwoVariantsOfECDHWithKDFTests.swift @@ -47,9 +47,12 @@ private struct Vector: Decodable { final class TwoVariantsOfECDHWithKDFTests: XCTestCase { - + override func setUp() { + super.setUp() + continueAfterFailure = false + } func testTwoVariantsOfECDHWithKDF_vectors() throws { - let fileURL = Bundle.module.url(forResource: "ecdh_secp256k1_two_variants_with_kdf_test", withExtension: ".json") + let fileURL = Bundle.module.url(forResource: "cyon_ecdh_two_variants_with_kdf", withExtension: ".json") let data = try Data(contentsOf: fileURL!) let suite = try JSONDecoder().decode(ECDHX963Suite.self, from: data) try suite.vectors.forEach(doTest) @@ -63,10 +66,10 @@ extension TwoVariantsOfECDHWithKDFTests { let outputByteCount = 32 let hash = SHA256.self - let alice = try PrivateKey(hex: vector.alicePrivateKey) - let bob = try PrivateKey(hex: vector.bobPrivateKey) - try XCTAssertEqual(alice.publicKey.rawRepresentation(format: .uncompressed).hex, vector.alicePublicKeyUncompressed) - try XCTAssertEqual(bob.publicKey.rawRepresentation(format: .uncompressed).hex, vector.bobPublicKeyUncompressed) + let alice = try K1.KeyAgreement.PrivateKey(hex: vector.alicePrivateKey) + let bob = try K1.KeyAgreement.PrivateKey(hex: vector.bobPrivateKey) + XCTAssertEqual(alice.publicKey.x963Representation.hex, vector.alicePublicKeyUncompressed) + XCTAssertEqual(bob.publicKey.x963Representation.hex, vector.bobPublicKeyUncompressed) for outcome in vector.outcomes { switch outcome.ecdhVariant { @@ -104,11 +107,11 @@ extension TwoVariantsOfECDHWithKDFTests { for derivedKeys in outcome.derivedKeys { let info = try XCTUnwrap(derivedKeys.info.data(using: .utf8)) let salt = try Data(hex: derivedKeys.salt) - let x963 = x963DerivedSymmetricKey(secret: sharedSecretAB, using: hash, sharedInfo: info, outputByteCount: outputByteCount) + let x963 = sharedSecretAB.x963DerivedSymmetricKey(using: hash, sharedInfo: info, outputByteCount: outputByteCount) x963.withUnsafeBytes { XCTAssertEqual(Data($0).hex, derivedKeys.x963, "Wrong X963 KDF result, mismatched expected from vector.") } - let hkdf = hkdfDerivedSymmetricKey(secret: sharedSecretAB, using: hash, salt: salt, sharedInfo: info, outputByteCount: outputByteCount) + let hkdf = sharedSecretAB.hkdfDerivedSymmetricKey(using: hash, salt: salt, sharedInfo: info, outputByteCount: outputByteCount) hkdf.withUnsafeBytes { XCTAssertEqual(Data($0).hex, derivedKeys.hkdf, "Wrong HKDF result, mismatched expected from vector.") } @@ -122,108 +125,3 @@ extension TwoVariantsOfECDHWithKDFTests { } - -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftCrypto open source project -// -// Copyright (c) 2019-2020 Apple Inc. and the SwiftCrypto project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.md for the list of SwiftCrypto project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -// Copy pasted over from https://github.com/apple/swift-crypto/blob/main/Sources/Crypto/Key%20Agreement/DH.swift#L48 - -/// Derives a symmetric encryption key using X9.63 key derivation. -/// -/// - Parameters: -/// - hashFunction: The Hash Function to use for key derivation. -/// - sharedInfo: The Shared Info to use for key derivation. -/// - outputByteCount: The length in bytes of resulting symmetric key. -/// - Returns: The derived symmetric key -public func x963DerivedSymmetricKey( - secret: Data, - using hashFunction: H.Type, sharedInfo: SI, outputByteCount: Int -) -> SymmetricKey { - // SEC1 defines 3 inputs to the KDF: - // - // 1. An octet string Z which is the shared secret value. That's `self` here. - // 2. An integer `keydatalen` which is the length in octets of the keying data to be generated. Here that's `outputByteCount`. - // 3. An optional octet string `SharedInfo` which consists of other shared data. Here, that's `sharedInfo`. - // - // We then need to perform the following steps: - // - // 1. Check that keydatalen < hashlen × (2³² − 1). If keydatalen ≥ hashlen × (2³² − 1), fail. - // 2. Initiate a 4 octet, big-endian octet string Counter as 0x00000001. - // 3. For i = 1 to ⌈keydatalen/hashlen⌉, do the following: - // 1. Compute: Ki = Hash(Z || Counter || [SharedInfo]). - // 2. Increment Counter. - // 3. Increment i. - // 4. Set K to be the leftmost keydatalen octets of: K1 || K2 || . . . || K⌈keydatalen/hashlen⌉. - // 5. Output K. - // - // The loop in step 3 is not very Swifty, so instead we generate the counter directly. - // Step 1: Check that keydatalen < hashlen × (2³² − 1). - // We do this math in UInt64-space, because we'll overflow 32-bit integers. - guard UInt64(outputByteCount) < (UInt64(H.Digest.byteCount) * UInt64(UInt32.max)) else { - fatalError("Invalid parameter size") - } - - var key = SecureBytes() - key.reserveCapacity(outputByteCount) - - var remainingBytes = outputByteCount - var counter = UInt32(1) - - while remainingBytes > 0 { - // 1. Compute: Ki = Hash(Z || Counter || [SharedInfo]). - var hasher = H() - hasher.update(data: secret) - hasher.update(counter.bigEndian) - hasher.update(data: sharedInfo) - let digest = hasher.finalize() - - // 2. Increment Counter. - counter += 1 - - // Append the bytes of the digest. We don't want to append more than the remaining number of bytes. - let bytesToAppend = min(remainingBytes, H.Digest.byteCount) - digest.withUnsafeBytes { digestPtr in - key.append(digestPtr.prefix(bytesToAppend)) - } - remainingBytes -= bytesToAppend - } - - precondition(key.count == outputByteCount) - return SymmetricKey(data: key) -} - -extension HashFunction { - mutating func update(_ counter: UInt32) { - withUnsafeBytes(of: counter) { - self.update(bufferPointer: $0) - } - } -} - - -/// From: https://github.com/apple/swift-crypto/blob/main/Sources/Crypto/Key%20Agreement/DH.swift#L110 -/// Derives a symmetric encryption key using HKDF key derivation. -/// -/// - Parameters: -/// - hashFunction: The Hash Function to use for key derivation. -/// - salt: The salt to use for key derivation. -/// - sharedInfo: The Shared Info to use for key derivation. -/// - outputByteCount: The length in bytes of resulting symmetric key. -/// - Returns: The derived symmetric key -public func hkdfDerivedSymmetricKey( - secret: Data, - using hashFunction: H.Type, salt: Salt, sharedInfo: SI, outputByteCount: Int -) -> SymmetricKey { - HKDF.deriveKey(inputKeyMaterial: SymmetricKey(data: secret), salt: salt, info: sharedInfo, outputByteCount: outputByteCount) -} diff --git a/Tests/K1Tests/TestCases/ECDSA/ECDSARecoverableSignatureRoundtripTests.swift b/Tests/K1Tests/TestCases/ECDSA/ECDSARecoverableSignatureRoundtripTests.swift index b97df30..ce4d8bc 100644 --- a/Tests/K1Tests/TestCases/ECDSA/ECDSARecoverableSignatureRoundtripTests.swift +++ b/Tests/K1Tests/TestCases/ECDSA/ECDSARecoverableSignatureRoundtripTests.swift @@ -11,10 +11,10 @@ import XCTest final class ECDSARecoverableSignatureRoundtripTests: XCTestCase { func testECDSARecoverable() throws { - let alice = try K1.PrivateKey.generateNew() + let alice = K1.ECDSA.Recoverable.PrivateKey() let message = "Send Bob 3 BTC".data(using: .utf8)! - let signature = try alice.ecdsaSignRecoverable(unhashed: message) - let isSignatureValid = try alice.publicKey.isValid(signature: signature, unhashed: message) + let signature = try alice.signature(forUnhashed: message) + let isSignatureValid = alice.publicKey.isValidSignature(signature, unhashed: message) XCTAssertTrue(isSignatureValid, "Signature should be valid.") } diff --git a/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTests.swift b/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTests.swift index a00fd6b..c2d1ccd 100644 --- a/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTests.swift +++ b/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTests.swift @@ -8,14 +8,40 @@ import Foundation import K1 import XCTest +import CryptoKit final class ECDSASignatureTests: XCTestCase { - func testECDSA() throws { - let alice = try K1.PrivateKey.generateNew() + func testECDSADeterministic() throws { + let alice = K1.ECDSA.NonRecoverable.PrivateKey() let message = "Send Bob 3 BTC".data(using: .utf8)! - let signature = try alice.ecdsaSignNonRecoverable(unhashed: message) - let isSignatureValid = try alice.publicKey.isValidECDSASignature(signature, unhashed: message) + + let signature = try alice.signature(forUnhashed: message) + + let isSignatureValid = alice.publicKey.isValidSignature(signature, unhashed: message) XCTAssertTrue(isSignatureValid, "Signature should be valid.") } + func testECDSARandom() throws { + continueAfterFailure = false + let alice = K1.ECDSA.NonRecoverable.PrivateKey() + let message = "Send Bob 3 BTC".data(using: .utf8)! + + let requestedNumberOfSignatures = 1000 + var signatures = Set() + + for i in 0.. ResultOfTestGroup { var numberOfTestsRun = 0 for vector in group.tests { - let privateKey = try K1.PrivateKey.import(rawRepresentation: Data(hex: vector.privateKey)) - let publicKey = privateKey.publicKey + let privateKey = try K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: Data(hex: vector.privateKey)) + let publicKey: K1.ECDSA.NonRecoverable.PublicKey = privateKey.publicKey let expectedSignature = try vector.expectedSignature() let messageDigest = try vector.messageDigest() - XCTAssertTrue(try publicKey.isValidECDSASignature(expectedSignature, digest: messageDigest)) + XCTAssertTrue(publicKey.isValidSignature(expectedSignature, digest: messageDigest)) - let signatureFromMessage = try privateKey.ecdsaSignNonRecoverable(digest: messageDigest) - let signatureRecoverableFromMessage = try privateKey.ecdsaSignRecoverable(digest: messageDigest) + let signatureFromMessage = try privateKey.signature(for: messageDigest) XCTAssertEqual(signatureFromMessage, expectedSignature) + + let signatureRandom = try privateKey.signature( + for: messageDigest, + options: .init(nonceFunction: .random) + ) + + XCTAssertNotEqual(signatureRandom, expectedSignature) + XCTAssertTrue(publicKey.isValidSignature(signatureRandom, digest: messageDigest)) + + let privateKeyRecoverable = try K1.ECDSA.Recoverable.PrivateKey(rawRepresentation: privateKey.rawRepresentation) + let signatureRecoverableFromMessage = try privateKeyRecoverable.signature(for: messageDigest) try XCTAssertEqual(signatureRecoverableFromMessage.nonRecoverable(), expectedSignature) let recid = try signatureRecoverableFromMessage.compact().recoveryID - XCTAssertEqual(signatureRecoverableFromMessage.rawRepresentation.hex, expectedSignature.rawRepresentation.hex + "\(Data([UInt8(recid)]).hex)") + XCTAssertEqual(signatureRecoverableFromMessage.rawRepresentation.hex, expectedSignature.rawRepresentation.hex + "\(Data([UInt8(recid.rawValue)]).hex)") numberOfTestsRun += 1 } return .init(numberOfTestsRun: numberOfTestsRun, idsOmittedTests: []) @@ -60,7 +71,7 @@ private extension XCTestCase { private struct SignatureTrezorTestVector: SignatureTestVector { typealias MessageDigest = SHA256.Digest - typealias Signature = ECDSASignatureNonRecoverable + typealias Signature = K1.ECDSA.NonRecoverable.Signature let msg: String let privateKey: String @@ -79,7 +90,7 @@ private struct SignatureTrezorTestVector: SignatureTestVector { } func expectedSignature() throws -> Signature { let derData = try Data(hex: expected.der) - let signature = try ECDSASignatureNonRecoverable.import(fromDER: derData) + let signature = try K1.ECDSA.NonRecoverable.Signature(derRepresentation: derData) try XCTAssertEqual(signature.derRepresentation().hex, expected.der) try XCTAssertEqual( signature.compactRepresentation().hex, @@ -89,6 +100,11 @@ private struct SignatureTrezorTestVector: SignatureTestVector { ].joined(separator: "") ) + try XCTAssertEqual( + K1.ECDSA.NonRecoverable.Signature(compactRepresentation: Data(hex: expected.r + expected.s)), + signature + ) + return signature } diff --git a/Tests/K1Tests/TestCases/ECDSA/ECDSAWycheproofASNDEREncodedSignaturesTests.swift b/Tests/K1Tests/TestCases/ECDSA/ECDSAWycheproofASNDEREncodedSignaturesTests.swift index 2b62344..0a3ddcb 100644 --- a/Tests/K1Tests/TestCases/ECDSA/ECDSAWycheproofASNDEREncodedSignaturesTests.swift +++ b/Tests/K1Tests/TestCases/ECDSA/ECDSAWycheproofASNDEREncodedSignaturesTests.swift @@ -24,12 +24,12 @@ final class ECDSA_Wycheproof_ASN_DER_EncodedSignaturesTests: XCTestCase { func testWycheProofSecp256k1_DER() throws { let result: TestResult = try testSuite( /* https://github.com/google/wycheproof/blob/master/testvectors/ecdsa_secp256k1_sha256_test.json */ - jsonName: "ecdsa_secp256k1_sha256_der_test", + jsonName: "wycheproof_ecdsa_verify_der", testFunction: { (group: ECDSAWycheTestGroup) in try doTestGroup( group: group, - signatureValidationMode: .acceptSignatureMalleability, + signatureValidationMode: .init(malleabilityStrictness: .accepted), hashFunction: SHA256.self, skipIfContainsFlags: .init(["MissingZero", "BER"]) ) @@ -42,7 +42,7 @@ final class ECDSA_Wycheproof_ASN_DER_EncodedSignaturesTests: XCTestCase { private struct SignatureWycheproofDERTestVector: WycheproofTestVector { typealias MessageDigest = SHA256.Digest - typealias Signature = ECDSASignatureNonRecoverable + typealias Signature = K1.ECDSA.NonRecoverable.Signature let comment: String let msg: String @@ -57,7 +57,7 @@ private struct SignatureWycheproofDERTestVector: WycheproofTestVector { } func expectedSignature() throws -> Signature { let derData = try Data(hex: sig) - let signature = try ECDSASignatureNonRecoverable.import(fromDER: derData) + let signature = try K1.ECDSA.NonRecoverable.Signature(derRepresentation: derData) if self.result == "valid" { try XCTAssertEqual(sig, signature.derRepresentation().hex) } diff --git a/Tests/K1Tests/TestCases/ECDSA/ECDSAWycheproofIEEEP1364RSEncodedSignaturesTests.swift b/Tests/K1Tests/TestCases/ECDSA/ECDSAWycheproofIEEEP1364RSEncodedSignaturesTests.swift index 9e66b0b..04c2a3c 100644 --- a/Tests/K1Tests/TestCases/ECDSA/ECDSAWycheproofIEEEP1364RSEncodedSignaturesTests.swift +++ b/Tests/K1Tests/TestCases/ECDSA/ECDSAWycheproofIEEEP1364RSEncodedSignaturesTests.swift @@ -9,11 +9,11 @@ final class ECDSA_Wycheproof_IEEE_P1364_RS_EncodedSignaturesTests: XCTestCase { let result: TestResult = try testSuite( /* https://github.com/google/wycheproof/blob/master/testvectors/ecdsa_secp256k1_sha256_test.json */ - jsonName: "ecdsa_secp256k1_sha256_p1363_RS_test", + jsonName: "wycheproof_ecdsa_verify_p1363", testFunction: { (group: ECDSAWycheTestGroup) in try doTestGroup( group: group, - signatureValidationMode: .acceptSignatureMalleability, + signatureValidationMode: .init(malleabilityStrictness: .accepted), hashFunction: SHA256.self, skipIfContainsFlags: .init(["MissingZero", "BER", "SigSize"]), skipIfContainsComment: ["r too large"] @@ -29,7 +29,7 @@ final class ECDSA_Wycheproof_IEEE_P1364_RS_EncodedSignaturesTests: XCTestCase { private struct SignatureWycheproofP1364TestVector: WycheproofTestVector { typealias MessageDigest = SHA256.Digest - typealias Signature = ECDSASignatureNonRecoverable + typealias Signature = K1.ECDSA.NonRecoverable.Signature let comment: String let msg: String diff --git a/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyEncodingTests.swift b/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyEncodingTests.swift new file mode 100644 index 0000000..c416a2e --- /dev/null +++ b/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyEncodingTests.swift @@ -0,0 +1,54 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-21. +// +import Foundation +@testable import K1 +import XCTest + + +final class PrivateKeyEncodingTests: XCTestCase { + + func testRawRoundtrip() throws { + try doTest( + serialize: \.rawRepresentation, + deserialize: K1.ECDSA.NonRecoverable.PrivateKey.init(rawRepresentation:) + ) + } + + func testx963Roundtrip() throws { + try doTest( + serialize: \.x963Representation, + deserialize: K1.ECDSA.NonRecoverable.PrivateKey.init(x963Representation:) + ) + } + + func testDERRoundtrip() throws { + try doTest( + serialize: \.derRepresentation, + deserialize: K1.ECDSA.NonRecoverable.PrivateKey.init(derRepresentation:) + ) + } + + func testPEMRoundtrip() throws { + try doTest( + serialize: \.pemRepresentation, + deserialize: K1.ECDSA.NonRecoverable.PrivateKey.init(pemRepresentation:) + ) + } +} + +private extension PrivateKeyEncodingTests { + func doTest( + serialize: KeyPath, + deserialize: (Enc) throws -> K1.ECDSA.NonRecoverable.PrivateKey + ) throws { + try doTestSerializationRoundtrip( + original: K1.ECDSA.NonRecoverable.PrivateKey(), + serialize: serialize, + deserialize: deserialize + ) + } +} diff --git a/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyGenerationTests.swift b/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyGenerationTests.swift index cf2cfd2..9f93c98 100644 --- a/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyGenerationTests.swift +++ b/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyGenerationTests.swift @@ -12,12 +12,12 @@ import XCTest final class PrivateKeyGenerationTests: XCTestCase { func testGenerationWorks() throws { - XCTAssertNoThrow(try PrivateKey.generateNew()) + XCTAssertNoThrow(K1.ECDSA.NonRecoverable.PrivateKey()) } func testRandom() throws { // The probability of two keys being identical is approximately: 1/2^256 - XCTAssertNotEqual(try PrivateKey.generateNew(), try PrivateKey.generateNew()) + XCTAssertNotEqual(K1.ECDSA.NonRecoverable.PrivateKey(), K1.ECDSA.NonRecoverable.PrivateKey()) } } diff --git a/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyImportTests.swift b/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyImportTests.swift index e5c3ede..f9d08f3 100644 --- a/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyImportTests.swift +++ b/Tests/K1Tests/TestCases/Keys/PrivateKey/PrivateKeyImportTests.swift @@ -14,23 +14,23 @@ final class PrivateKeyImportTests: XCTestCase { func testAssertImportingPrivateKeyWithTooFewBytesThrowsError() throws { let raw = try Data(hex: "deadbeef") assert( - try PrivateKey.import(rawRepresentation: raw), - throws: K1.Error.invalidSizeOfPrivateKey(providedByteCount: raw.count) + try K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: raw), + throws: K1.Error.failedToInitializePrivateKeyIncorrectByteCount(got: 4, expected: 32) ) } func testAssertImportingPrivateKeyWithTooManyBytesThrowsError() throws { let raw = Data(repeating: 0xba, count: 33) assert( - try PrivateKey.import(rawRepresentation: raw), - throws: K1.Error.invalidSizeOfPrivateKey(providedByteCount: raw.count) + try K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: raw), + throws: K1.Error.failedToInitializePrivateKeyIncorrectByteCount(got: 33, expected: 32) ) } func testAssertImportingPrivateKeyZeroThrowsError() throws { let raw = Data(repeating: 0x00, count: 32) assert( - try PrivateKey.import(rawRepresentation: raw), + try K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: raw), throws: K1.Error.invalidPrivateKeyMustNotBeZero ) } @@ -38,7 +38,7 @@ final class PrivateKeyImportTests: XCTestCase { func testAssertImportingPrivateKeyCurveOrderThrowsError() throws { let raw = try Data(hex: "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141") assert( - try PrivateKey.import(rawRepresentation: raw), + try K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: raw), throws: K1.Error.invalidPrivateKeyMustBeSmallerThanOrder ) } @@ -46,17 +46,17 @@ final class PrivateKeyImportTests: XCTestCase { func testAssertImportingPrivateKeyLargerThanCurveOrderThrowsError() throws { let raw = Data(repeating: 0xff, count: 32) assert( - try PrivateKey.import(rawRepresentation: raw), + try K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: raw), throws: K1.Error.invalidPrivateKeyMustBeSmallerThanOrder ) } func testAssertPublicKeyOfImportedPrivateKey1() throws { let privateKeyRaw = try Data(hex: "0000000000000000000000000000000000000000000000000000000000000001") - let privateKey = try K1.PrivateKey.import(rawRepresentation: privateKeyRaw) + let privateKey = try K1.ECDSA.NonRecoverable.PrivateKey(rawRepresentation: privateKeyRaw) // Easily verified by: https://bitaddress.org/ // Pretty well known key pair - let expectedPublicKey = try K1.PublicKey.import(from: Data(hex: "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8")) + let expectedPublicKey = try K1.ECDSA.NonRecoverable.PublicKey(x963Representation: Data(hex: "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8")) XCTAssertEqual(privateKey.publicKey, expectedPublicKey) } diff --git a/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyEncodingTests.swift b/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyEncodingTests.swift new file mode 100644 index 0000000..3337af5 --- /dev/null +++ b/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyEncodingTests.swift @@ -0,0 +1,91 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-21. +// + +import Foundation +@testable import K1 +import XCTest + + +final class PublicKeyEncodingTests: XCTestCase { + + func test_pubkey_raw_is_x963_minus_prefix() throws { + let privateKey = K1.ECDSA.NonRecoverable.PrivateKey() + let publicKey = privateKey.publicKey + + XCTAssertEqual(publicKey.rawRepresentation.hex, Data(publicKey.x963Representation.dropFirst()).hex) + } + + func testRawRoundtrip() throws { + try doTest( + serialize: \.rawRepresentation, + deserialize: K1.ECDSA.NonRecoverable.PublicKey.init(rawRepresentation:) + ) + } + + func testCompressedRoundtrip() throws { + try doTest( + serialize: \.compressedRepresentation, + deserialize: K1.ECDSA.NonRecoverable.PublicKey.init(compressedRepresentation:) + ) + } + + func testx963Roundtrip() throws { + try doTest( + serialize: \.x963Representation, + deserialize: K1.ECDSA.NonRecoverable.PublicKey.init(x963Representation:) + ) + } + + func testDERRoundtrip() throws { + try doTest( + serialize: \.derRepresentation, + deserialize: K1.ECDSA.NonRecoverable.PublicKey.init(derRepresentation:) + ) + } + + func testPEMRoundtrip() throws { + try doTest( + serialize: \.pemRepresentation, + deserialize: K1.ECDSA.NonRecoverable.PublicKey.init(pemRepresentation:) + ) + } +} + +private extension PublicKeyEncodingTests { + func doTest( + serialize: KeyPath, + deserialize: (Enc) throws -> K1.ECDSA.NonRecoverable.PublicKey + ) throws { + try doTestSerializationRoundtrip( + original: K1.ECDSA.NonRecoverable.PublicKey.generateNew(), + serialize: serialize, + deserialize: deserialize + ) + } +} + +extension K1.ECDSA.NonRecoverable.PublicKey { + static func generateNew() -> Self { + K1.ECDSA.NonRecoverable.PrivateKey().publicKey + } +} + +public func doTestSerializationRoundtrip( + original makeOriginal: @autoclosure () -> T, + serialize: KeyPath, + deserialize: (Enc) throws -> T +) throws where T: Equatable, Enc: Equatable { + for _ in 0 ..< 100 { + let original = makeOriginal() + let serialized = original[keyPath: serialize] + let deserialized = try deserialize(serialized) + XCTAssertEqual(deserialized, original) + let reserialized = deserialized[keyPath: serialize] + XCTAssertEqual(reserialized, serialized) + + } +} diff --git a/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyImportTests.swift b/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyImportTests.swift index 0f1251e..929d6cf 100644 --- a/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyImportTests.swift +++ b/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyImportTests.swift @@ -13,46 +13,47 @@ final class PublicKeyImportTests: XCTestCase { func testAssertImportingPublicKeyWithTooFewBytesThrowsError() throws { let raw = try Data(hex: "deadbeef") assert( - try PublicKey.import(from: raw), - throws: K1.Error.incorrectByteCountOfPublicKey(providedByteCount: raw.count) + try K1.ECDSA.NonRecoverable.PublicKey.init(x963Representation: raw), + throws: K1.Error.incorrectByteCountOfX963PublicKey(got: 4, expected: 65) ) } func testAssertImportingPublicKeyWithTooManyBytesThrowsError() throws { let raw = Data(repeating: 0xde, count: 66) assert( - try PublicKey.import(from: raw), - throws: K1.Error.incorrectByteCountOfPublicKey(providedByteCount: raw.count) + try K1.ECDSA.NonRecoverable.PublicKey(x963Representation: raw), + throws: K1.Error.incorrectByteCountOfX963PublicKey(got: 66, expected: 65) ) } func testAssertImportingInvalidUncompressedPublicKeyThrowsError() throws { let raw = Data(repeating: 0x04, count: 65) assert( - try PublicKey.import(from: raw), - throws: K1.Error.failedToParsePublicKeyFromBytes + try K1.ECDSA.NonRecoverable.PublicKey(x963Representation: raw), + throws: K1.Error.failedToDeserializePublicKey ) } func testAssertImportingInvalidCompressedPublicKeyThrowsError() throws { let raw = Data(repeating: 0x03, count: 33) assert( - try PublicKey.import(from: raw), - throws: K1.Error.failedToParsePublicKeyFromBytes + try K1.ECDSA.NonRecoverable.PublicKey(compressedRepresentation: raw), + throws: K1.Error.failedToDeserializePublicKey ) } func testAssertImportValidPublicKeyWorks() throws { let raw = Data(repeating: 0x02, count: 33) - let publicKey = try PublicKey.import(from: raw) - XCTAssertEqual(publicKey.uncompressedRaw.hex, "040202020202020202020202020202020202020202020202020202020202020202415456f0fc01d66476251cab4525d9db70bfec652b2d8130608675674cde64b2") + let publicKey = try K1.ECDSA.NonRecoverable.PublicKey(compressedRepresentation: raw) + XCTAssertEqual(publicKey.compressedRepresentation.hex, "020202020202020202020202020202020202020202020202020202020202020202") + XCTAssertEqual(publicKey.x963Representation.hex, "040202020202020202020202020202020202020202020202020202020202020202415456f0fc01d66476251cab4525d9db70bfec652b2d8130608675674cde64b2") } func test_compress_pubkey() throws { let raw = Data(repeating: 0x02, count: 33) - let publicKey = try PublicKey.import(from: raw) - try XCTAssertEqual(publicKey.rawRepresentation(format: .compressed).hex, "020202020202020202020202020202020202020202020202020202020202020202") - try XCTAssertEqual(publicKey.rawRepresentation(format: .uncompressed).hex, "040202020202020202020202020202020202020202020202020202020202020202415456f0fc01d66476251cab4525d9db70bfec652b2d8130608675674cde64b2") + let publicKey = try K1.ECDSA.NonRecoverable.PublicKey(compressedRepresentation: raw) + XCTAssertEqual(publicKey.compressedRepresentation.hex, "020202020202020202020202020202020202020202020202020202020202020202") + XCTAssertEqual(publicKey.x963Representation.hex, "040202020202020202020202020202020202020202020202020202020202020202415456f0fc01d66476251cab4525d9db70bfec652b2d8130608675674cde64b2") } func testNotOnCurve() throws { @@ -63,8 +64,8 @@ final class PublicKeyImportTests: XCTestCase { let raw = try Data(hex: "040000000000000000000000000000000000000000000000000000000000000000fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e") assert( - try PublicKey.import(from: raw), - throws: K1.Error.failedToParsePublicKeyFromBytes + try K1.ECDSA.NonRecoverable.PublicKey(x963Representation: raw), + throws: K1.Error.failedToDeserializePublicKey ) } } diff --git a/Tests/K1Tests/TestCases/Performance/PerformanceTests.swift b/Tests/K1Tests/TestCases/Performance/PerformanceTests.swift new file mode 100644 index 0000000..dbe5abf --- /dev/null +++ b/Tests/K1Tests/TestCases/Performance/PerformanceTests.swift @@ -0,0 +1,88 @@ +import Foundation +import K1 +import XCTest + +final class PerformanceTests: XCTestCase { + + func testPerformance() { + let message = [UInt8](repeating: 0xab, count: 32) + measure { + do { + for _ in 0 ..< 10 { + let schnorrPrivateKey = K1.Schnorr.PrivateKey() + let schnorrPublicKey = schnorrPrivateKey.publicKey + + try XCTAssertEqual( + K1.Schnorr.PublicKey(compressedRepresentation: schnorrPublicKey.compressedRepresentation), + schnorrPublicKey + ) + try XCTAssertEqual( + K1.Schnorr.PublicKey(x963Representation: schnorrPublicKey.x963Representation), + schnorrPublicKey + ) + + let ecdsaPrivateKey = K1.ECDSA.Recoverable.PrivateKey() + let ecdsaPublicKey = ecdsaPrivateKey.publicKey + + try XCTAssertEqual( + K1.ECDSA.Recoverable.PublicKey(compressedRepresentation: ecdsaPublicKey.compressedRepresentation), + ecdsaPublicKey + ) + try XCTAssertEqual( + K1.ECDSA.Recoverable.PublicKey(x963Representation: ecdsaPublicKey.x963Representation), + ecdsaPublicKey + ) + + let ecdsa = try ecdsaPrivateKey.signature(for: message) + XCTAssertTrue( + ecdsaPublicKey.isValidSignature( + ecdsa, + hashed: message + ) + ) + try XCTAssertEqual( + K1.ECDSA.Recoverable.Signature(compact: ecdsa.compact()), + ecdsa + ) + try XCTAssertEqual( + K1.ECDSA.NonRecoverable.Signature(compactRepresentation: ecdsa.nonRecoverable().compactRepresentation()), + ecdsa.nonRecoverable() + ) + try XCTAssertEqual( + K1.ECDSA.NonRecoverable.Signature(derRepresentation: ecdsa.nonRecoverable().derRepresentation()), + ecdsa.nonRecoverable() + ) + + let schnorr = try schnorrPrivateKey.signature(for: message) + XCTAssertTrue( + schnorrPublicKey.isValidSignature( + schnorr, + hashed: message + ) + ) + try XCTAssertEqual( + K1.Schnorr.Signature(rawRepresentation: schnorr.rawRepresentation), + schnorr + ) + + let alicePrivateKey = try K1.KeyAgreement.PrivateKey(x963Representation: ecdsaPrivateKey.x963Representation) + let alicePublicKey = alicePrivateKey.publicKey + let bobPrivateKey = K1.KeyAgreement.PrivateKey() + let bobPublicKey = bobPrivateKey.publicKey + + var ab = try alicePrivateKey.sharedSecretFromKeyAgreement(with: bobPublicKey) + var ba = try bobPrivateKey.sharedSecretFromKeyAgreement(with: alicePublicKey) + XCTAssertEqual(ab, ba) + ab = try alicePrivateKey.ecdh(with: bobPublicKey) + ba = try bobPrivateKey.ecdh(with: alicePublicKey) + XCTAssertEqual(ab, ba) + } + } catch { + XCTFail("abort") + } + } + + } + +} + diff --git a/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift b/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift index 992a9e2..35a7c23 100644 --- a/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift +++ b/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift @@ -9,6 +9,7 @@ import Foundation @testable import K1 import XCTest + /// Test vectors: /// https://gist.github.com/webmaster128/130b628d83621a33579751846699ed15 final class ECDASignaturePublicKeyRecoveryTests: XCTestCase { @@ -20,7 +21,7 @@ final class ECDASignaturePublicKeyRecoveryTests: XCTestCase { func test_recovery_test_vectors() throws { let result: TestResult = try testSuite( - jsonName: "publickey_recovery", + jsonName: "warta_cyon_publickey_recovery", testFunction: { (group: RecoveryTestGroup) in try doTestGroup(group: group) } @@ -32,16 +33,41 @@ final class ECDASignaturePublicKeyRecoveryTests: XCTestCase { func test_conversionRoundtrips() throws { let recoverySignatureHex = "acf9e195e094f2f40eb619b9878817ff951b9b11fac37cf0d7290098bbefb574f8606281a2231a3fc781045f2ea4df086936263bbfa8d15ca17fe70e0c3d6e5601" let recoverableSigRaw = try Data(hex: recoverySignatureHex) - let recoverableSig = try ECDSASignatureRecoverable(rawRepresentation: recoverableSigRaw) + let recoverableSig = try K1.ECDSA.Recoverable.Signature(rawRepresentation: recoverableSigRaw) + XCTAssertEqual(recoverableSig.rawRepresentation.hex, recoverySignatureHex) + + let compactRSV = "74b5efbb980029d7f07cc3fa119b1b95ff178887b919b60ef4f294e095e1f9ac566e3d0c0ee77fa15cd1a8bf3b26366908dfa42e5f0481c73f1a23a2816260f801" + try XCTAssertEqual(recoverableSig.compact().serialize(format: .rsv).hex, compactRSV) + let compactVRS = "0174b5efbb980029d7f07cc3fa119b1b95ff178887b919b60ef4f294e095e1f9ac566e3d0c0ee77fa15cd1a8bf3b26366908dfa42e5f0481c73f1a23a2816260f8" + try XCTAssertEqual(recoverableSig.compact().serialize(format: .vrs).hex, compactVRS) + + try XCTAssertEqual( + recoverableSig.rawRepresentation.hex, + K1.ECDSA.Recoverable.Signature(compact: .init(rawRepresentation: Data(hex: compactVRS), format: .vrs)).rawRepresentation.hex + ) + let compactRecoverableSig = try recoverableSig.compact() - XCTAssertEqual(compactRecoverableSig.rs.hex, "74b5efbb980029d7f07cc3fa119b1b95ff178887b919b60ef4f294e095e1f9ac566e3d0c0ee77fa15cd1a8bf3b26366908dfa42e5f0481c73f1a23a2816260f8") - XCTAssertEqual(compactRecoverableSig.recoveryID, 1) - let nonRecoverable = try ECDSASignatureNonRecoverable(compactRepresentation: compactRecoverableSig.rs) + let compactRecoverableSigRSHex = "74b5efbb980029d7f07cc3fa119b1b95ff178887b919b60ef4f294e095e1f9ac566e3d0c0ee77fa15cd1a8bf3b26366908dfa42e5f0481c73f1a23a2816260f8" + let recid = try K1.ECDSA.Recoverable.Signature.RecoveryID(recid: 1) + XCTAssertEqual(compactRecoverableSig.compact.hex, compactRecoverableSigRSHex) + XCTAssertEqual(compactRecoverableSig.recoveryID, recid) + + let compactRecoverableSigRS = try Data(hex: compactRecoverableSigRSHex) + try XCTAssertEqual(K1.ECDSA.Recoverable.Signature(compact: compactRecoverableSigRS, recoveryID: recid), K1.ECDSA.Recoverable.Signature(compact: compactRecoverableSig)) + try XCTAssertEqual(K1.ECDSA.Recoverable.Signature.Compact.init(compact: compactRecoverableSigRS, recoveryID: recid), compactRecoverableSig) + + let nonRecoverable = try K1.ECDSA.NonRecoverable.Signature(compactRepresentation: compactRecoverableSig.compact) try XCTAssertEqual(nonRecoverable, recoverableSig.nonRecoverable()) let nonRecovDer = try nonRecoverable.derRepresentation() - XCTAssertEqual(nonRecovDer.hex, "3044022074b5efbb980029d7f07cc3fa119b1b95ff178887b919b60ef4f294e095e1f9ac0220566e3d0c0ee77fa15cd1a8bf3b26366908dfa42e5f0481c73f1a23a2816260f8") + let nonRecoveryDERHex = "3044022074b5efbb980029d7f07cc3fa119b1b95ff178887b919b60ef4f294e095e1f9ac0220566e3d0c0ee77fa15cd1a8bf3b26366908dfa42e5f0481c73f1a23a2816260f8" + XCTAssertEqual(nonRecovDer.hex, nonRecoveryDERHex) + + try XCTAssertEqual(K1.ECDSA.NonRecoverable.Signature(derRepresentation: Data(hex: nonRecoveryDERHex)), nonRecoverable) + + + } } @@ -54,12 +80,11 @@ private extension ECDASignaturePublicKeyRecoveryTests { var numberOfTestsRun = 0 for vector in group.tests { let publicKeyUncompressed = try [UInt8](hex: vector.publicKeyUncompressed) - let expectedPublicKey = try K1.PublicKey( - wrapped: .init(uncompressedRaw: publicKeyUncompressed) - ) + let expectedPublicKey = try K1.ECDSA.Recoverable.PublicKey.init(x963Representation: publicKeyUncompressed) + XCTAssertEqual( - try [UInt8](hex: vector.publicKeyCompressed), - try expectedPublicKey.rawRepresentation(format: .compressed) + try Data(hex: vector.publicKeyCompressed), + expectedPublicKey.compressedRepresentation ) @@ -67,25 +92,22 @@ private extension ECDASignaturePublicKeyRecoveryTests { try XCTAssertEqual(recoverableSig.compact().recoveryID, vector.recoveryID) let hashedMessage = try Data(hex: vector.hashMessage) - XCTAssertTrue(try expectedPublicKey.isValid(signature: recoverableSig, hashed: hashedMessage)) - XCTAssertTrue(try expectedPublicKey.isValid(signature: recoverableSig.nonRecoverable(), hashed: hashedMessage)) + XCTAssertTrue(expectedPublicKey.isValidSignature(recoverableSig, hashed: hashedMessage)) try XCTAssertEqual(vector.recoveryID, recoverableSig.compact().recoveryID) - + let recoveredPublicKey = try recoverableSig.recoverPublicKey( - messageThatWasSigned: hashedMessage + message: hashedMessage ) - + XCTAssertEqual(expectedPublicKey, recoveredPublicKey) - - XCTAssertTrue(try recoveredPublicKey.isValid(signature: recoverableSig, hashed: hashedMessage)) - XCTAssertTrue(try recoveredPublicKey.isValid(signature: recoverableSig.nonRecoverable(), hashed: hashedMessage)) - + + XCTAssertTrue(recoveredPublicKey.isValidSignature(recoverableSig, hashed: hashedMessage)) + let recoveredWithID = try recoverableSig.nonRecoverable().recoverPublicKey( recoveryID: vector.recoveryID, - messageThatWasSigned: hashedMessage + message: hashedMessage ) - XCTAssertEqual(expectedPublicKey, recoveredWithID) - + try XCTAssertEqual(expectedPublicKey, .init(x963Representation: recoveredWithID.x963Representation)) numberOfTestsRun += 1 } @@ -99,19 +121,25 @@ private struct RecoveryTestGroup: Decodable { struct IncorrectByteCount: Swift.Error {} struct RecoveryTestVector: Decodable, Equatable { - let recoveryID: Int + let recoveryID: K1.ECDSA.Recoverable.Signature.RecoveryID let message: String let hashMessage: String private let signature: String - func recoverableSignature() throws -> ECDSASignatureRecoverable { - let raw = try Data(hex: self.signature) - let signature = try ECDSASignatureRecoverable(rawRepresentation: raw) - XCTAssertEqual(signature.rawRepresentation, raw) - return signature + func recoverableSignature() throws -> K1.ECDSA.Recoverable.Signature { + try K1.ECDSA.Recoverable.Signature( + rawRepresentation: Data(hex: signature) + ) } let publicKeyUncompressed: String let publicKeyCompressed: String } + +extension K1.ECDSA.Recoverable.Signature.RecoveryID: ExpressibleByIntegerLiteral { + public init(integerLiteral value: UInt8) { + self.init(rawValue: value)! + } +} + diff --git a/Tests/K1Tests/TestCases/Schnorr/SchnorrSignatureBitcoinCoreTests.swift b/Tests/K1Tests/TestCases/Schnorr/SchnorrSignatureBitcoinCoreTests.swift index ad2a7fe..67b1bff 100644 --- a/Tests/K1Tests/TestCases/Schnorr/SchnorrSignatureBitcoinCoreTests.swift +++ b/Tests/K1Tests/TestCases/Schnorr/SchnorrSignatureBitcoinCoreTests.swift @@ -60,7 +60,7 @@ final class SchnorrSignatureBitcoinCoreTests: XCTestCase { func testSchnorrSignBitcoinVectors() throws { let result: TestResult = try testSuite( /* https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv */ - jsonName: "schnorr_secp256k1_sign_sha256_test", + jsonName: "bip340_schnorr_sign", testFunction: { (group: SchnorrTestGroup) in var numberOfTestsRun = 0 try group.tests.forEach(doTestSchnorrSign) @@ -75,7 +75,7 @@ final class SchnorrSignatureBitcoinCoreTests: XCTestCase { let result: TestResult = try testSuite( /* https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv */ - jsonName: "schnorr_secp256k1_verify_sha256_test", + jsonName: "bip340_schnorr_verify", testFunction: { (group: SchnorrTestGroup) in var numberOfTestsRun = 0 try group.tests.forEach(doTestSchnorrVerify) @@ -90,43 +90,43 @@ final class SchnorrSignatureBitcoinCoreTests: XCTestCase { private extension SchnorrSignatureBitcoinCoreTests { func doTestSchnorrSign(vector: SchnorrTestSignVector) throws { - let privateKey = try K1.PrivateKey.import( + let privateKey = try K1.Schnorr.PrivateKey( rawRepresentation: Data(hex: vector.privateKeyHex) ) let publicKey = privateKey.publicKey - let expectedPublicKey = try K1.PublicKey.import(from: Data(hex: vector.publicKeyHex)) + let expectedPublicKey = try K1.Schnorr.PublicKey(compressedRepresentation: Data(hex: vector.publicKeyHex)) XCTAssertEqual(publicKey, expectedPublicKey) XCTAssertEqual( - try! publicKey.rawRepresentation(format: .compressed).hex.lowercased(), - try! expectedPublicKey.rawRepresentation(format: .compressed).hex.lowercased() - ) + publicKey.compressedRepresentation, + expectedPublicKey.compressedRepresentation + ) let message = try Data(hex: vector.messageHex) - let signature = try privateKey.schnorrSign( - hashed: message, - input: .init(auxilaryRandomData: Data(hex: vector.auxDataHex)) + let signature = try privateKey.signature( + for: message, + options: .init(auxilaryRandomData: .init(aux: Data(hex: vector.auxDataHex))) ) - let expectedSig = try SchnorrSignature(rawRepresentation: Data(hex: vector.signatureCompact)) + let expectedSig = try K1.Schnorr.Signature(rawRepresentation: Data(hex: vector.signatureCompact)) - XCTAssertEqual(signature, expectedSig) + XCTAssertEqual(signature.rawRepresentation.hex, vector.signatureCompact) XCTAssertEqual( - try publicKey.isValidSchnorrSignature(expectedSig, hashed: message), + publicKey.isValidSignature(expectedSig, hashed: message), vector.isValid ) } func doTestSchnorrVerify(vector: SchnorrTestVector) throws { - func parsePublicKey() throws -> PublicKey { - try PublicKey.import(from: Data(hex: vector.publicKeyHex)) + func parsePublicKey() throws -> K1.Schnorr.PublicKey { + try .init(compressedRepresentation: Data(hex: vector.publicKeyHex)) } guard !vector.invalidPublicKey else { XCTAssertThrowsError(try parsePublicKey(), "") { anyError in if let error = anyError as? K1.Error { - XCTAssertEqual(error, K1.Error.failedToParsePublicKeyFromBytes) + XCTAssertEqual(error, K1.Error.failedToDeserializePublicKey) } else { XCTFail("Failed to cast error") } @@ -135,9 +135,9 @@ private extension SchnorrSignatureBitcoinCoreTests { } let publicKey = try parsePublicKey() - let signature = try SchnorrSignature(rawRepresentation: Data(hex: vector.signatureCompact)) + let signature = try K1.Schnorr.Signature(rawRepresentation: Data(hex: vector.signatureCompact)) - let validSignature = try publicKey.isValidSchnorrSignature( + let validSignature = try publicKey.isValidSignature( signature, hashed: Data(hex: vector.messageHex) ) diff --git a/Tests/K1Tests/TestCases/Schnorr/SchnorrSignatureTests.swift b/Tests/K1Tests/TestCases/Schnorr/SchnorrSignatureTests.swift index 87b9ec4..e401a37 100644 --- a/Tests/K1Tests/TestCases/Schnorr/SchnorrSignatureTests.swift +++ b/Tests/K1Tests/TestCases/Schnorr/SchnorrSignatureTests.swift @@ -8,14 +8,15 @@ import Foundation import K1 import XCTest +import CryptoKit final class SchnorrSignatureTests: XCTestCase { func testSchnorr() throws { - let alice = try K1.PrivateKey.generateNew() + let alice = K1.Schnorr.PrivateKey() let message = "Send Bob 3 BTC".data(using: .utf8)! - let signature = try alice.schnorrSign(unhashed: message) - let isSignatureValid = try alice.publicKey.isValidSchnorrSignature(signature, unhashed: message) + let signature = try alice.signature(forUnhashed: message) + let isSignatureValid = alice.publicKey.isValidSignature(signature, unhashed: message) XCTAssertTrue(isSignatureValid, "Signature should be valid.") } diff --git a/Tests/K1Tests/TestVectors/schnorr_secp256k1_sign_sha256_test.json b/Tests/K1Tests/TestVectors/bip340_schnorr_sign.json similarity index 100% rename from Tests/K1Tests/TestVectors/schnorr_secp256k1_sign_sha256_test.json rename to Tests/K1Tests/TestVectors/bip340_schnorr_sign.json diff --git a/Tests/K1Tests/TestVectors/schnorr_secp256k1_verify_sha256_test.json b/Tests/K1Tests/TestVectors/bip340_schnorr_verify.json similarity index 100% rename from Tests/K1Tests/TestVectors/schnorr_secp256k1_verify_sha256_test.json rename to Tests/K1Tests/TestVectors/bip340_schnorr_verify.json diff --git a/Tests/K1Tests/TestVectors/ecdh_secp256k1_two_variants_with_kdf_test.json b/Tests/K1Tests/TestVectors/cyon_ecdh_two_variants_with_kdf.json similarity index 100% rename from Tests/K1Tests/TestVectors/ecdh_secp256k1_two_variants_with_kdf_test.json rename to Tests/K1Tests/TestVectors/cyon_ecdh_two_variants_with_kdf.json diff --git a/Tests/K1Tests/TestVectors/ecdsa_secp256k1_sha256_rfc6979_trezor_test.json b/Tests/K1Tests/TestVectors/trezor_ecdsa_sign_rfc6979.json similarity index 100% rename from Tests/K1Tests/TestVectors/ecdsa_secp256k1_sha256_rfc6979_trezor_test.json rename to Tests/K1Tests/TestVectors/trezor_ecdsa_sign_rfc6979.json diff --git a/Tests/K1Tests/TestVectors/publickey_recovery.json b/Tests/K1Tests/TestVectors/warta_cyon_publickey_recovery.json similarity index 99% rename from Tests/K1Tests/TestVectors/publickey_recovery.json rename to Tests/K1Tests/TestVectors/warta_cyon_publickey_recovery.json index 083358e..3f411cf 100644 --- a/Tests/K1Tests/TestVectors/publickey_recovery.json +++ b/Tests/K1Tests/TestVectors/warta_cyon_publickey_recovery.json @@ -1,5 +1,7 @@ { "algorithm": "ecdsa public key recovery", + "authors": ["Simon Warta", "Alexander Cyon"], + "origin": "", "numberOfTests": 10, "testGroups": [ { diff --git a/Tests/K1Tests/TestVectors/ecdh_secp256k1_test.json b/Tests/K1Tests/TestVectors/wycheproof_ecdh_ASN1x963.json similarity index 100% rename from Tests/K1Tests/TestVectors/ecdh_secp256k1_test.json rename to Tests/K1Tests/TestVectors/wycheproof_ecdh_ASN1x963.json diff --git a/Tests/K1Tests/TestVectors/ecdsa_secp256k1_sha256_der_test.json b/Tests/K1Tests/TestVectors/wycheproof_ecdsa_verify_der.json similarity index 100% rename from Tests/K1Tests/TestVectors/ecdsa_secp256k1_sha256_der_test.json rename to Tests/K1Tests/TestVectors/wycheproof_ecdsa_verify_der.json diff --git a/Tests/K1Tests/TestVectors/ecdsa_secp256k1_sha256_p1363_RS_test.json b/Tests/K1Tests/TestVectors/wycheproof_ecdsa_verify_p1363.json similarity index 100% rename from Tests/K1Tests/TestVectors/ecdsa_secp256k1_sha256_p1363_RS_test.json rename to Tests/K1Tests/TestVectors/wycheproof_ecdsa_verify_p1363.json diff --git a/Tests/K1Tests/Util/Data+Hex.swift b/Tests/K1Tests/Util/Data+Hex.swift deleted file mode 100644 index aca0755..0000000 --- a/Tests/K1Tests/Util/Data+Hex.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-02-01. -// - -import Foundation -@testable import K1 - -public extension Data { - init(hex: String) throws { - try self.init(Array(hex: hex)) - } - -} diff --git a/Tests/K1Tests/Util/ECSignature.swift b/Tests/K1Tests/Util/ECSignature.swift new file mode 100644 index 0000000..df0c297 --- /dev/null +++ b/Tests/K1Tests/Util/ECSignature.swift @@ -0,0 +1,13 @@ +// +// File.swift +// +// +// Created by Alexander Cyon on 2023-03-19. +// + +import Foundation +import K1 + +public protocol ECSignature {} +extension K1.ECDSA.NonRecoverable.Signature: ECSignature {} +extension K1.ECDSA.Recoverable.Signature: ECSignature {} diff --git a/Tests/K1Tests/Util/PublicKey+DERDecode.swift b/Tests/K1Tests/Util/PublicKey+DERDecode.swift deleted file mode 100644 index e5e4942..0000000 --- a/Tests/K1Tests/Util/PublicKey+DERDecode.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// File.swift -// -// -// Created by Alexander Cyon on 2022-01-31. -// - -import Foundation -@testable import K1 - -import ASN1Decoder - -extension K1.PublicKey { - enum WycheproofDERDecodeError: Swift.Error { - case expectedFirstElementToBeSequence - case expectedTopLevelObjectToHaveSeqeuenceTag - case expectedTopLevelSequenceWithElementCount(of: Int, butGot: Int) - case elementLacksIdentifier(elementIndex: Int) - case expectedBitString(atIndex: Int) - case failedToCastBitstringToData - } - - private static func importPublicKeyDataFromASN1Object(der: Data) throws -> Data { - - let asn1Objects = try ASN1DERDecoder.decode(data: der) - - guard asn1Objects.count == 1 else { - throw WycheproofDERDecodeError.expectedTopLevelSequenceWithElementCount(of: 1, butGot: asn1Objects.count) - } - - guard - let topLevelObjectIdentifier = asn1Objects[0].identifier, - case let topLevelObjectTag = topLevelObjectIdentifier.tagNumber(), - topLevelObjectTag == .sequence else { - throw WycheproofDERDecodeError.expectedTopLevelObjectToHaveSeqeuenceTag - } - - - let bitstringObjectElementIndex = 1 - let expectedElementCount = bitstringObjectElementIndex + 1 - - guard asn1Objects[0].subCount() == expectedElementCount else { - throw WycheproofDERDecodeError.expectedTopLevelSequenceWithElementCount(of: expectedElementCount, butGot: asn1Objects.count) - } - - guard let bitstringObject = asn1Objects[0].sub(bitstringObjectElementIndex) else { - throw WycheproofDERDecodeError.expectedBitString(atIndex: bitstringObjectElementIndex) - } - - guard let bitstringObjectIdentifier = bitstringObject.identifier else { - throw WycheproofDERDecodeError.elementLacksIdentifier(elementIndex: bitstringObjectElementIndex) - } - - guard - case let bitstringObjectTag = bitstringObjectIdentifier.tagNumber(), - bitstringObjectTag == .bitString - else { - throw WycheproofDERDecodeError.expectedBitString(atIndex: bitstringObjectElementIndex) - } - - guard let bitstringData = bitstringObject.value as? Data else { - throw WycheproofDERDecodeError.failedToCastBitstringToData - } - - return bitstringData - - } - - - static func `import`(der: Data) throws -> Self { - let publicKeyData = try importPublicKeyDataFromASN1Object(der: der) - return try Self.import(from: publicKeyData) - } -} diff --git a/Tests/K1Tests/Util/Wycheproof.swift b/Tests/K1Tests/Util/Wycheproof.swift index cdef77e..568d83f 100644 --- a/Tests/K1Tests/Util/Wycheproof.swift +++ b/Tests/K1Tests/Util/Wycheproof.swift @@ -16,6 +16,7 @@ //===----------------------------------------------------------------------===// import XCTest import CryptoKit + @testable import K1 struct TestSuite: Decodable { @@ -65,13 +66,11 @@ struct ResultOfTestGroup { let idsOmittedTests: [Int] } - - extension XCTestCase { func doTestGroup( group: ECDSAWycheTestGroup, - signatureValidationMode: SignatureValidationMode, + signatureValidationMode: K1.ECDSA.ValidationOptions = .default, hashFunction: HF.Type, skipIfContainsFlags: [String] = [], skipIfContainsComment: [String] = [], @@ -83,7 +82,42 @@ extension XCTestCase { throw ECDSASignatureTestError(description: errorMessage) } let keyBytes = try Array(hex: group.key.uncompressed) - let key = try PublicKey(x963Representation: keyBytes) + let key = try K1.ECDSA.NonRecoverable.PublicKey(x963Representation: keyBytes) + + let keyFromDER = try K1.ECDSA.NonRecoverable.PublicKey(derRepresentation: Data(hex: group.keyDer)) + XCTAssertEqual(key.derRepresentation.hex, group.keyDer) + XCTAssertEqual(keyFromDER, key) + + let keyFromPEM = try K1.ECDSA.NonRecoverable.PublicKey(pemRepresentation: group.keyPem) + XCTAssertEqual(key.pemRepresentation, group.keyPem) + XCTAssertEqual(keyFromPEM, key) + + + let compactXRaw = try Data(hex: group.key.wx) + let compactYRaw = try Data(hex: group.key.wy) + func ensure32Bytes(_ compactComponent: Data) throws -> Data { + if compactComponent.count == Curve.Field.byteCount { + return compactComponent + } + + var compactComponent = [UInt8](compactComponent) + while compactComponent.count < Curve.Field.byteCount { + compactComponent = [0x00] + compactComponent + } + while compactComponent.count > Curve.Field.byteCount { + guard compactComponent.first == 0x00 else { + throw BadKeyComponent() + } + compactComponent = [UInt8](compactComponent.dropFirst()) + } + return Data(compactComponent) + } + let xOnly = try ensure32Bytes(compactXRaw) + let yOnly = try ensure32Bytes(compactYRaw) + + let fromRaw = try K1.ECDSA.NonRecoverable.PublicKey(rawRepresentation: xOnly + yOnly) + XCTAssertEqual(fromRaw, key) + var numberOfTestsRun = 0 var idsOfOmittedTests = Array() outerloop: for testVector in group.tests { @@ -106,10 +140,10 @@ extension XCTestCase { let signature = try testVector.expectedSignature() let messageDigest = try testVector.messageDigest() - isValid = try key.isValidECDSASignature( + isValid = key.isValidSignature( signature, digest: messageDigest, - mode: signatureValidationMode + options: signatureValidationMode ) } catch { let expectedFailure = testVector.result == "invalid" || testVector.result == "acceptable" @@ -152,21 +186,26 @@ struct ECDSATestGroup: Codable { struct ECDSAWycheTestGroup: Codable { let tests: [TV] let key: ECDSAKey + let keyDer: String + let keyPem: String } struct ECDSAKey: Codable { let uncompressed: String + let wx: String + let wy: String let curve: String } + protocol SignatureTestVector: Codable { associatedtype MessageDigest: Digest associatedtype Signature: ECSignature func messageDigest() throws -> MessageDigest func expectedSignature() throws -> Signature } -protocol WycheproofTestVector: SignatureTestVector where Signature == ECDSASignatureNonRecoverable { +protocol WycheproofTestVector: SignatureTestVector where Signature == K1.ECDSA.NonRecoverable.Signature { var flags: [String] { get } var tcId: Int { get } var result: String { get } @@ -175,15 +214,8 @@ protocol WycheproofTestVector: SignatureTestVector where Signature == ECDSASigna } -typealias PublicKey = K1.PublicKey -extension PublicKey { - init(x963Representation: [UInt8]) throws { - self = try Self.import(from: x963Representation) - } -} -typealias PrivateKey = K1.PrivateKey - struct ECDSASignatureTestError: Swift.Error, CustomStringConvertible { let description: String } +struct BadKeyComponent: Swift.Error {} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 86364e5..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1 +0,0 @@ -fatalError("Please use --enable-test-discovery to run the tests instead") diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..daf5136 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,11 @@ +#!/bin/bash + + +set -eu +LIB="Sources/secp256k1/libsecp256k1" +here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$here"/.. +rm -rf $LIB + +git submodule init +git submodule update