diff --git a/README.md b/README.md index 5f6b3f6..808b0cb 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,21 @@ let reference = key.reference let originalData = key.originalData ``` +### Use X.509 certificate +SwiftyRSA supports X.509 certificate for public keys. SwiftyRSA can add the X.509 header to a headerless public key, or on the contrary strip it to get a key without a header. +#### Add an X.509 header to a public key +```swift +let publicKey = PublicKey(data: data) +let publicKeyData = try publicKey.data() +let publicKey_with_X509_header = try SwiftyRSA.prependX509KeyHeader(keyData: publicKeyData) +``` +#### Strip the X.509 header from a public key +```swift +let publicKey_headerLess: Data = try SwiftyRSA.stripKeyHeader(keyData: publicKey_with_X509_header) +``` + +**Warning** : Storing (with SwiftyRSA's methods) or creating a ```PublicKey``` instance will automatically strip the header from the key. For more info, see *Under the hood* above. + Create public and private RSA keys ---------------------------------- diff --git a/Source/SwiftyRSA.swift b/Source/SwiftyRSA.swift index 73cbff9..7fbfba0 100644 --- a/Source/SwiftyRSA.swift +++ b/Source/SwiftyRSA.swift @@ -3,6 +3,7 @@ // SwiftyRSA // // Created by Loïs Di Qual on 7/2/15. +// Contributions by Stchepinsky Nathan on 24/06/2021 // Copyright (c) 2015 Scoop Technologies, Inc. All rights reserved. // @@ -285,6 +286,36 @@ public enum SwiftyRSA { throw SwiftyRSAError.invalidAsn1Structure } + /** + This method prepend the x509 header to the given PublicKey data. + If the key already contain a x509 header, the given data is returned as is. + It letterally does the opposite of the previous method : + From a given headerless key : + SEQUENCE + INTEGER (1024 or 2048 bit) -- modulo + INTEGER -- public exponent + the key is returned following the X509 header : + SEQUENCE + SEQUENCE + OBJECT IDENTIFIER 1.2.840.113549.1.1.1 + NULL + BIT STRING + SEQUENCE + INTEGER (1024 or 2048 bit) -- modulo + INTEGER -- public exponent + */ + + static func prependX509KeyHeader(keyData : Data) throws -> Data{ + if try keyData.isAnHeaderlessKey(){ + let x509certificate : Data = keyData.prependx509Header() + return x509certificate + } else if try keyData.hasX509Header() { + return keyData + } else { // invalideHeader + throw SwiftyRSAError.x509CertificateFailed + } + } + static func removeKey(tag: String) { guard let tagData = tag.data(using: .utf8) else { diff --git a/Source/SwiftyRSAError.swift b/Source/SwiftyRSAError.swift index 37751c2..27a2d1a 100644 --- a/Source/SwiftyRSAError.swift +++ b/Source/SwiftyRSAError.swift @@ -3,6 +3,7 @@ // SwiftyRSA // // Created by Lois Di Qual on 5/15/17. +// Contributions by Stchepinsky Nathan on 24/06/2021 // Copyright © 2017 Scoop. All rights reserved. // @@ -32,6 +33,7 @@ public enum SwiftyRSAError: Error { case derFileNotFound(name: String) case notAPublicKey case notAPrivateKey + case x509CertificateFailed var localizedDescription: String { switch self { @@ -79,6 +81,8 @@ public enum SwiftyRSAError: Error { return "Provided key is not a valid RSA public key" case .notAPrivateKey: return "Provided key is not a valid RSA pivate key" + case .x509CertificateFailed : + return "Couldn't prepend the provided key because it has an unexpected structure" } } } diff --git a/Source/X509Certificate.swift b/Source/X509Certificate.swift new file mode 100644 index 0000000..07191a7 --- /dev/null +++ b/Source/X509Certificate.swift @@ -0,0 +1,171 @@ +// +// X509Certificate.swift +// SwiftyRSA +// +// Created by Stchepinsky Nathan on 24/06/2021. +// Copyright © 2021 Scoop. All rights reserved. +// + +import Foundation + + +/// +/// Encoding/Decoding lengths as octets +/// +private extension NSInteger { + func encodedOctets() -> [CUnsignedChar] { + // Short form + if self < 128 { + return [CUnsignedChar(self)]; + } + + // Long form + let i = Int(log2(Double(self)) / 8 + 1) + var len = self + var result: [CUnsignedChar] = [CUnsignedChar(i + 0x80)] + + for _ in 0..> 8 + } + + return result + } + + init?(octetBytes: [CUnsignedChar], startIdx: inout NSInteger) { + if octetBytes[startIdx] < 128 { + // Short form + self.init(octetBytes[startIdx]) + startIdx += 1 + } else { + // Long form + let octets = NSInteger(octetBytes[startIdx] as UInt8 - 128) + + if octets > octetBytes.count - startIdx { + self.init(0) + return nil + } + + var result = UInt64(0) + + for j in 1...octets { + result = (result << 8) + result = result + UInt64(octetBytes[startIdx + j]) + } + + startIdx += 1 + octets + self.init(result) + } + } +} + + + +public extension Data{ + // This code source come from Heimdall project https://github.com/henrinormak/Heimdall published under MIT Licence + + /// This method prepend the X509 header to a given public key + func prependx509Header() -> Data { + let result = NSMutableData() + + let encodingLength: Int = (self.count + 1).encodedOctets().count + let OID: [CUnsignedChar] = [0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, + 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00] + + var builder: [CUnsignedChar] = [] + + // ASN.1 SEQUENCE + builder.append(0x30) + + // Overall size, made of OID + bitstring encoding + actual key + let size = OID.count + 2 + encodingLength + self.count + let encodedSize = size.encodedOctets() + builder.append(contentsOf: encodedSize) + result.append(builder, length: builder.count) + result.append(OID, length: OID.count) + builder.removeAll(keepingCapacity: false) + + builder.append(0x03) + builder.append(contentsOf: (self.count + 1).encodedOctets()) + builder.append(0x00) + result.append(builder, length: builder.count) + + // Actual key bytes + result.append(self) + + return result as Data + } + + func hasX509Header() throws -> Bool{ + let node: Asn1Parser.Node + do { + node = try Asn1Parser.parse(data: self) + } catch { + throw SwiftyRSAError.asn1ParsingFailed + } + + + // Ensure the raw data is an ASN1 sequence + guard case .sequence(let nodes) = node else { + return false + } + + // Must contain 2 elements, a sequence and a bit string + if nodes.count != 2 { + return false + } + + // Ensure the first node is an ASN1 sequence + guard case .sequence(let firstNode) = nodes[0] else { + return false + } + + // Must contain 2 elements, an object id and NULL + if firstNode.count != 2 { + return false + } + + guard case .objectIdentifier(_) = firstNode[0] else { + return false + } + + guard case .null = firstNode[1] else { + return false + } + + // The 2sd child has to be a bit string containing a sequence of 2 int + + + let last = nodes[1] + if case .bitString(let secondChildSequence) = last { + return try secondChildSequence.isAnHeaderlessKey() + } else { + return false + } + } + + func isAnHeaderlessKey() throws -> Bool{ + let node: Asn1Parser.Node + do { + node = try Asn1Parser.parse(data: self) + } catch { + throw SwiftyRSAError.asn1ParsingFailed + } + + // Ensure the raw data is an ASN1 sequence + guard case .sequence(let nodes) = node else { + return false + } + + // Detect whether the sequence only has integers, in which case it's a headerless key + let onlyHasIntegers = nodes.filter { node -> Bool in + if case .integer = node { + return false + } + return true + }.isEmpty + + // Headerless key + return onlyHasIntegers + } +} diff --git a/SwiftyRSA.xcodeproj/project.pbxproj b/SwiftyRSA.xcodeproj/project.pbxproj index b816ba2..37b2591 100644 --- a/SwiftyRSA.xcodeproj/project.pbxproj +++ b/SwiftyRSA.xcodeproj/project.pbxproj @@ -10,6 +10,13 @@ 2C9144BB1528A03A53582ADA158D7F71 /* NSData+SHA.m in Sources */ = {isa = PBXBuildFile; fileRef = D77B6899031A54A9F698D3430A599421 /* NSData+SHA.m */; }; 47C6D29039697B1E71B856F957188617 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CEBB75D7D70F1A095D22C1D7BFD8773 /* Security.framework */; }; 61C3EEC5859298BF282B0BA572B8FCAC /* NSData+SHA.h in Headers */ = {isa = PBXBuildFile; fileRef = 5AAC360BC9A1D1773D59C8E91186044E /* NSData+SHA.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 902623FE2687278D00489B07 /* X509Certificate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 902623FD2687278D00489B07 /* X509Certificate.swift */; }; + 902623FF2687278D00489B07 /* X509Certificate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 902623FD2687278D00489B07 /* X509Certificate.swift */; }; + 902624002687278D00489B07 /* X509Certificate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 902623FD2687278D00489B07 /* X509Certificate.swift */; }; + 90EBFC8526AB093B002B30E9 /* X509Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90EBFC8326AB08DF002B30E9 /* X509Tests.swift */; }; + 90EBFC8626AB093C002B30E9 /* X509Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90EBFC8326AB08DF002B30E9 /* X509Tests.swift */; }; + 90EBFC8926AC32B3002B30E9 /* swiftyrsa-public-base64-X509-format.txt in Resources */ = {isa = PBXBuildFile; fileRef = 90EBFC8726AC32B2002B30E9 /* swiftyrsa-public-base64-X509-format.txt */; }; + 90EBFC8A26AC32B3002B30E9 /* swiftyrsa-public-base64-X509-format.txt in Resources */ = {isa = PBXBuildFile; fileRef = 90EBFC8726AC32B2002B30E9 /* swiftyrsa-public-base64-X509-format.txt */; }; B0981AE770653C8FE25311592E2BCE5E /* SwiftyRSA.h in Headers */ = {isa = PBXBuildFile; fileRef = 1CB44E2E8BE22C702325440A0F96410A /* SwiftyRSA.h */; settings = {ATTRIBUTES = (Private, ); }; }; BF52D5FDEDA5DFCA62ECD03992601E82 /* SwiftyRSA.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E84F551B29670A456D12366AA4C8E5 /* SwiftyRSA.swift */; }; C034317B1EC2D7E30019B7E8 /* Asn1Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C034317A1EC2D7E30019B7E8 /* Asn1Parser.swift */; }; @@ -113,6 +120,9 @@ 5AAC360BC9A1D1773D59C8E91186044E /* NSData+SHA.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+SHA.h"; sourceTree = ""; }; 6E2D7A1B9C4D58D0887FB023C0E15594 /* swiftyrsa-private-headerless.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "swiftyrsa-private-headerless.pem"; sourceTree = ""; }; 7DF0A3222330CDFCF12918ED08519EB9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 902623FD2687278D00489B07 /* X509Certificate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = X509Certificate.swift; sourceTree = ""; }; + 90EBFC8326AB08DF002B30E9 /* X509Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = X509Tests.swift; sourceTree = ""; }; + 90EBFC8726AC32B2002B30E9 /* swiftyrsa-public-base64-X509-format.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "swiftyrsa-public-base64-X509-format.txt"; sourceTree = ""; }; 986B7C763D1B85C628621E89E7071B06 /* swiftyrsa-public.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "swiftyrsa-public.der"; sourceTree = ""; }; 9E21744A2073DB8AC7BAEB324D36D4DB /* TestUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = ""; }; AC84097FE3BCDEAEE6370ADD41E9AE68 /* swiftyrsa-public-headerless.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "swiftyrsa-public-headerless.pem"; sourceTree = ""; }; @@ -221,6 +231,7 @@ D77B6899031A54A9F698D3430A599421 /* NSData+SHA.m */, C0E83E041ECD7DC400E9D63C /* PrivateKey.swift */, C0E83DFC1ECD7DA300E9D63C /* PublicKey.swift */, + 902623FD2687278D00489B07 /* X509Certificate.swift */, C096A00B1D90754E00E285C2 /* Signature.swift */, 38E84F551B29670A456D12366AA4C8E5 /* SwiftyRSA.swift */, C06A33DA1E6E815A000EE6DC /* SwiftyRSA+ObjC.swift */, @@ -244,6 +255,7 @@ children = ( D240C55056247A7B270C92143EF30532 /* keys */, C096A00F1D9083D000E285C2 /* KeyTests.swift */, + 90EBFC8326AB08DF002B30E9 /* X509Tests.swift */, C096A0131D9086F300E285C2 /* MessageTests.swift */, C096A0151D9092C700E285C2 /* EncryptDecryptTests.swift */, C096A0171D90969A00E285C2 /* SignatureTests.swift */, @@ -261,6 +273,7 @@ C065ED6E1DBF314500674763 /* swiftyrsa-private.der */, C096A0111D9085C100E285C2 /* swiftyrsa-public-base64.txt */, AD7F28E3936301CA24DF13E22196C73B /* multiple-keys-testcase.pem */, + 90EBFC8726AC32B2002B30E9 /* swiftyrsa-public-base64-X509-format.txt */, 47807FD4AF81104758D4EA9B778F5186 /* multiple-keys-testcase.sh */, 6E2D7A1B9C4D58D0887FB023C0E15594 /* swiftyrsa-private-headerless.pem */, EF193719CB8D4DDE8D347FF699311D1D /* swiftyrsa-private.pem */, @@ -495,6 +508,7 @@ C0579268215465A60057F401 /* swiftyrsa-private-headerless.pem in Resources */, C057926C215465A60057F401 /* swiftyrsa-public.pem in Resources */, C057926D215465A60057F401 /* swiftyrsa-private-header-octetstring.pem in Resources */, + 90EBFC8A26AC32B3002B30E9 /* swiftyrsa-public-base64-X509-format.txt in Resources */, C0579269215465A60057F401 /* swiftyrsa-private.pem in Resources */, C057926B215465A60057F401 /* swiftyrsa-public.der in Resources */, C0579265215465A60057F401 /* swiftyrsa-public-base64.txt in Resources */, @@ -513,6 +527,7 @@ C076F5531DADEC1D006AF5DB /* swiftyrsa-public-headerless.pem in Resources */, C04D33811DC54D5700D65B05 /* swiftyrsa-public-base64-newlines.txt in Resources */, C084A72A1EC3034B003F79ED /* swiftyrsa-private-header-octetstring.pem in Resources */, + 90EBFC8926AC32B3002B30E9 /* swiftyrsa-public-base64-X509-format.txt in Resources */, C076F5541DADEC1D006AF5DB /* swiftyrsa-public.der in Resources */, C076F5551DADEC1D006AF5DB /* swiftyrsa-public.pem in Resources */, C065ED6F1DBF314500674763 /* swiftyrsa-private.der in Resources */, @@ -544,6 +559,7 @@ buildActionMask = 2147483647; files = ( C0E83E0D1ECD7F4200E9D63C /* EncryptedMessage.swift in Sources */, + 902623FE2687278D00489B07 /* X509Certificate.swift in Sources */, C096A00C1D90754E00E285C2 /* Signature.swift in Sources */, 2C9144BB1528A03A53582ADA158D7F71 /* NSData+SHA.m in Sources */, C096A0081D9071CA00E285C2 /* Message.swift in Sources */, @@ -563,6 +579,7 @@ buildActionMask = 2147483647; files = ( C057922321545CB00057F401 /* ClearMessage.swift in Sources */, + 902624002687278D00489B07 /* X509Certificate.swift in Sources */, C057922421545CB00057F401 /* Asn1Parser.swift in Sources */, C057922521545CB00057F401 /* EncryptedMessage.swift in Sources */, C057922621545CB00057F401 /* Key.swift in Sources */, @@ -582,6 +599,7 @@ buildActionMask = 2147483647; files = ( C0579242215465350057F401 /* EncryptedMessage.swift in Sources */, + 902623FF2687278D00489B07 /* X509Certificate.swift in Sources */, C057924A215465350057F401 /* SwiftyRSA+ObjC.swift in Sources */, C0579243215465350057F401 /* Key.swift in Sources */, C0579249215465350057F401 /* SwiftyRSA.swift in Sources */, @@ -603,6 +621,7 @@ C05792612154659B0057F401 /* TestUtils.swift in Sources */, C05792602154659B0057F401 /* SignatureTests.swift in Sources */, C057925E2154659B0057F401 /* MessageTests.swift in Sources */, + 90EBFC8626AB093C002B30E9 /* X509Tests.swift in Sources */, C057925D2154659B0057F401 /* KeyTests.swift in Sources */, C05792622154659B0057F401 /* ObjCTests.m in Sources */, C057925F2154659B0057F401 /* EncryptDecryptTests.swift in Sources */, @@ -616,6 +635,7 @@ C076F5481DADEBC2006AF5DB /* KeyTests.swift in Sources */, C076F5491DADEBC2006AF5DB /* MessageTests.swift in Sources */, C076F54A1DADEBC2006AF5DB /* EncryptDecryptTests.swift in Sources */, + 90EBFC8526AB093B002B30E9 /* X509Tests.swift in Sources */, C076F54B1DADEBC2006AF5DB /* SignatureTests.swift in Sources */, C076F54C1DADEBC2006AF5DB /* TestUtils.swift in Sources */, C076F54D1DADEBC2006AF5DB /* ObjCTests.m in Sources */, diff --git a/SwiftyRSA.xcodeproj/xcshareddata/xcschemes/SwiftyRSA iOS.xcscheme b/SwiftyRSA.xcodeproj/xcshareddata/xcschemes/SwiftyRSA iOS.xcscheme index 6f14514..15a368f 100644 --- a/SwiftyRSA.xcodeproj/xcshareddata/xcschemes/SwiftyRSA iOS.xcscheme +++ b/SwiftyRSA.xcodeproj/xcshareddata/xcschemes/SwiftyRSA iOS.xcscheme @@ -40,8 +40,17 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - codeCoverageEnabled = "YES" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> + + + + @@ -54,17 +63,6 @@ - - - - - - - -