javaswiftcryptographywalmart-api

How to create a signature from an RSA private key in Swift


I'm trying to port this Java code to Swift to create a signature from private key.

The private key is generated using these instructions.

https://walmart.io/key-tutorial

openssl genrsa -des3 -out WM_IO_my_rsa_key_pair 2048 

https://walmart.io/docs/affiliate/onboarding-guide

public String generateSignature(String key, String stringToSign) throws Exception {
        Signature signatureInstance = Signature.getInstance("SHA256WithRSA");

        ServiceKeyRep keyRep = new ServiceKeyRep(KeyRep.Type.PRIVATE, "RSA", "PKCS#8", Base64.decodeBase64(key));

        PrivateKey resolvedPrivateKey = (PrivateKey) keyRep.readResolve();

        signatureInstance.initSign(resolvedPrivateKey);

        byte[] bytesToSign = stringToSign.getBytes("UTF-8");
        signatureInstance.update(bytesToSign);
        byte[] signatureBytes = signatureInstance.sign();

        String signatureString = Base64.encodeBase64String(signatureBytes);

        return signatureString;
    }

This is my attempt, but it's not working. It fails at SecKeyCreateWithData.

func createHash(string: String) -> Data {
    let hash = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(CC_SHA256_DIGEST_LENGTH))
    let hashLength = string.count
    defer { hash.deallocate() }
    CC_SHA256(string, CC_LONG(hashLength), hash)
    return Data(bytes: hash, count: hashLength)
}

func createSignature(string: String) throws -> String? {
    guard let url = Bundle.module.url(forResource: "WM_IO_private_key", withExtension: "pem") else {
        return nil
    }
    
    let privateKey = try String(contentsOf: url, encoding: .utf8)
        .replacingOccurrences(of: "-----BEGIN PRIVATE KEY-----", with: "")
        .replacingOccurrences(of: "-----END PRIVATE KEY-----", with: "")
        .split(separator: "\n").joined()
    
    var error: Unmanaged<CFError>?
    guard let privateKeyData = Data(base64Encoded: privateKey, options: .ignoreUnknownCharacters) else {
        return nil
    }
    
    let attributes: [NSObject : NSObject] = [
       kSecAttrKeyType: kSecAttrKeyTypeRSA,
       kSecAttrKeyClass: kSecAttrKeyClassPrivate,
       kSecAttrKeySizeInBits: NSNumber(value: 2048),
       kSecReturnPersistentRef: true as NSObject
    ]
    
    guard let secKey = SecKeyCreateWithData(privateKeyData as CFData, attributes as CFDictionary, &error) else {
        return nil
    }
    
    let hash = createHash(string: string)
    let algorithm: SecKeyAlgorithm = .rsaSignatureDigestPSSSHA256
    guard let signature = SecKeyCreateSignature(secKey, algorithm, hash as CFData, &error) as Data? else {
        throw error!.takeRetainedValue() as Error
    }
    
    return signature.base64EncodedString()
}

Solution

  • The commando you used to create the key encrypts the key. As far as I know, this is not supported by SecKeyCreateWithData. You can remove the encryption by running the following command and entering your password.

    openssl rsa -in WM_IO_private_key -out WM_IO_private_key
    

    Remember, the private key is now unencrypted.

    func signature(string: String) throws -> String? {
        guard let url = Bundle.module.url(forResource: "WM_IO_private_key", withExtension: "") else {
            return nil
        }
    
        let derString = try String(contentsOf: url)
            .replacingOccurrences(of: "-----BEGIN RSA PRIVATE KEY-----", with: "")
            .replacingOccurrences(of: "-----END RSA PRIVATE KEY-----", with: "")
            .replacingOccurrences(of: "\n", with: "")
    
        guard let derData = Data(base64Encoded: derString, options: .ignoreUnknownCharacters) else {
            return nil
        }
    
        let attributes: [CFString: Any] = [
            kSecClass: kSecClassKey,
            kSecAttrKeyType: kSecAttrKeyTypeRSA,
            kSecAttrKeyClass: kSecAttrKeyClassPrivate,
            kSecAttrKeySizeInBits: 2048
        ]
    
        var error: Unmanaged<CFError>?
        guard let privateKey = SecKeyCreateWithData(derData as CFData, attributes as CFDictionary, &error) else {
            throw error!.takeUnretainedValue()
        }
    
        // Instead of creating the hash yourself, let the security framework do it for you
        // by using the rsaSignatureMessagePKCS1v15SHA256 algorithm. Alternatively you could
        // use rsaSignatureMessagePSSSHA256, as PSS is a better algorithm.
    
        // Make sure the key supports signing with the algorithm
        guard SecKeyIsAlgorithmSupported(privateKey, .sign, .rsaSignatureMessagePKCS1v15SHA256) else {
            return nil
        }
        guard let signature = SecKeyCreateSignature(privateKey, .rsaSignatureMessagePKCS1v15SHA256, Data(string.utf8) as CFData, &error) else {
            throw error!.takeUnretainedValue()
        }
        return (signature as Data).base64EncodedString()
    }