0

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()
}
Berry Blue
  • 15,330
  • 18
  • 62
  • 113
  • What does the `&error` tell you? `print(error?.takeUnretainedValue())`. Have you tried removing `\r` from the PEM file too? Also, try updating the attributes to a `[CFString: Any]` type, might also be a problem for the function. And finally, remove `kSecReturnPersistentRef` attribute, I don't think the `SecKeyCreateWithData` wants any return attribute – Bram Sep 13 '22 at 22:35
  • Thanks! I wasn't sure how to print the error. `RSA private key creation from data failed` – Berry Blue Sep 14 '22 at 15:27
  • Sounds to me like the data still is invalid. Like I mentioned in my comment, make sure you strip `\r` too and remove the `kSecReturnPersistentRef` from the attributes. I think that should do it – Bram Sep 14 '22 at 15:45
  • 1
    Ah, your openssl commando also encrypts the key. That's not supported by the `SecKeyCreateWithData` as far as I know. You could try to create a `.p12` container from the key instead, or remove the password from the key using `openssl rsa -in WM_IO_private_key -out WM_IO_private_key`. This will prompt your password and output the unencrypted private key – Bram Sep 14 '22 at 17:33
  • Thank you @Bram!! That seems to have worked. Please post your comment as an answer so I can accept. – Berry Blue Sep 14 '22 at 18:55
  • So, I'm still having a problem with SecKeyCreateSignature. It keeps giving me a different value even with the same input. I don't think I'm calling that one correctly. – Berry Blue Sep 14 '22 at 22:13
  • No, that's the PSS signature scheme. It inserts a random value every time, so that's correct behaviour. You could try setting a PKCS#1 signature instead, I believe that one outputs the same signature every time if I recall correctly. Which of the comments is the one that solved your issue? – Bram Sep 14 '22 at 22:44
  • Yes, that's the part I'm still stuck on is how to sign the signature. Your comment for SecKeyCreateWithData helped me solve the issue there! – Berry Blue Sep 15 '22 at 00:26

2 Answers2

1

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()
}
Bram
  • 2,718
  • 1
  • 22
  • 43
  • Thank you! This worked to create the key, but I'm still having difficulty implementing the signing algorithm. – Berry Blue Sep 15 '22 at 15:31
  • What's the issue with the signature? Like I commented, PSS uses a random to further harden and randomise the signature output. Having different outputs for the same data is expected – Bram Sep 16 '22 at 07:57
  • The way the signature is generated with the Java code above, it's the same signature with the same inputs. It's not random. – Berry Blue Sep 16 '22 at 13:40
  • There you go. The `SHA256WithRSA` algorithm is a different implementation than PSS. You can read more about it over [here](https://stackoverflow.com/questions/21018355/sha256withrsa-what-does-it-do-and-in-what-order) – Bram Sep 16 '22 at 14:34
0

As of today, auth signature code is available in Java (https://www.walmart.io/docs/affiliate/onboarding-guide) The idea we provided sample code to help the customers to implement the logic at customer end by referring it. You can implement the logic in(C# .NET, Python, PHP or JS) in such a way that whenever your system invoking Walmart APIs, generate the signature on the fly and pass as input parameter. This is how all of customers implemented and consuming our APIs.

Please refer the below documentation for complete. https://walmart.io/docs/affiliate/quick-start-guide https://www.walmart.io/docs/affiliate/onboarding-guide

Regards, Firdos IO Support