From d0191fc84794720fa77606b6d9f0dc3f77ac2744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20=C4=8Eurech?= Date: Wed, 28 Aug 2024 10:44:23 +0200 Subject: [PATCH] Core: Support for ECDSA JOSE signatures (#614) --- include/PowerAuth/PublicTypes.h | 39 ++++- include/PowerAuth/Session.h | 3 +- .../security/powerauth/core/Session.java | 3 +- .../powerauth/core/SignatureFormat.java | 54 +++++++ .../security/powerauth/core/SignedData.java | 9 +- .../security/powerauth/sdk/PowerAuthSDK.java | 22 ++- .../PowerAuthCore/PowerAuthCoreSession.h | 3 +- .../PowerAuthCore/PowerAuthCoreSession.mm | 4 +- proj-xcode/PowerAuthCore/PowerAuthCoreTypes.h | 35 ++++- .../PowerAuthCore/PowerAuthCoreTypes.mm | 13 +- src/PowerAuth/Session.cpp | 20 ++- src/PowerAuth/crypto/ECC.cpp | 139 +++++++++++++++++- src/PowerAuth/crypto/ECC.h | 9 ++ src/PowerAuth/jni/SessionJNI.cpp | 26 ++-- src/PowerAuth/utils/DataReader.cpp | 32 ++++ src/PowerAuth/utils/DataReader.h | 7 +- src/PowerAuth/utils/DataWriter.cpp | 29 ++++ src/PowerAuth/utils/DataWriter.h | 5 + src/PowerAuthTests/pa2CryptoECDSATests.cpp | 6 + .../pa2DataWriterReaderTests.cpp | 52 +++++++ src/PowerAuthTests/pa2SessionTests.cpp | 2 +- 21 files changed, 480 insertions(+), 32 deletions(-) create mode 100644 proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/SignatureFormat.java diff --git a/include/PowerAuth/PublicTypes.h b/include/PowerAuth/PublicTypes.h index 6792861a..529e7f02 100644 --- a/include/PowerAuth/PublicTypes.h +++ b/include/PowerAuth/PublicTypes.h @@ -386,24 +386,53 @@ namespace powerAuth HMAC_Activation = 3 }; + enum SignatureFormat + { + /** + If default signature is used, then `ECDSA_DER` is used for ECDSA signature. + The raw bytes are always used for HMAC signatures. + */ + Default = 0, + /** + ECDSA signature in DER format is expected at input, or produced at output: + ``` + ASN.1 notation: + ECDSASignature ::= SEQUENCE { + r INTEGER, + s INTEGER + } + ``` + */ + ECDSA_DER = 1, + /** + ECDSA signature in JOSE format is epxpected at input, or produced at output. + */ + ECDSA_JOSE = 2, + }; + /** A key type used for signature calculation. */ SigningKey signingKey; /** - An arbitrary data + Format of signature expected at input, or produced at output. + */ + SignatureFormat signatureFormat; + /** + An arbitrary data. */ cc7::ByteArray data; /** - A signagure calculated for data + A signagure calculated for data. */ cc7::ByteArray signature; /** - Default constructor + Default constructor. */ - SignedData(SigningKey signingKey = ECDSA_MasterServerKey) : - signingKey(signingKey) + SignedData(SigningKey signingKey = ECDSA_MasterServerKey, SignatureFormat signatureFormat = Default) : + signingKey(signingKey), + signatureFormat(signatureFormat) { } diff --git a/include/PowerAuth/Session.h b/include/PowerAuth/Session.h index e4c39271..31f1dbc7 100644 --- a/include/PowerAuth/Session.h +++ b/include/PowerAuth/Session.h @@ -429,7 +429,8 @@ namespace powerAuth EC_WrongParam, if some required parameter is missing */ ErrorCode signDataWithDevicePrivateKey(const std::string & c_vault_key, const SignatureUnlockKeys & keys, - const cc7::ByteRange & data, cc7::ByteArray & out_signature); + const cc7::ByteRange & data, SignedData::SignatureFormat out_format, + cc7::ByteArray & out_signature); private: diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/Session.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/Session.java index d222ea37..516d2d6d 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/Session.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/Session.java @@ -477,10 +477,11 @@ public byte[] prepareKeyValueDictionaryForDataSigning(Map keyVal * @param cVaultKey encrypted vault key * @param unlockKeys unlock keys object with required possession factor * @param data data to be signed + * @param signatureFormat Format of produced signature. * * @return array of bytes with calculated signature or null in case of failure. */ - public native byte[] signDataWithDevicePrivateKey(String cVaultKey, SignatureUnlockKeys unlockKeys, byte[] data); + public native byte[] signDataWithDevicePrivateKey(String cVaultKey, SignatureUnlockKeys unlockKeys, byte[] data, @SignatureFormat int signatureFormat); // // External encryption key diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/SignatureFormat.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/SignatureFormat.java new file mode 100644 index 00000000..3385988e --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/SignatureFormat.java @@ -0,0 +1,54 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * 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. + */ + +package io.getlime.security.powerauth.core; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import static io.getlime.security.powerauth.core.SignatureFormat.DEFAULT; +import static io.getlime.security.powerauth.core.SignatureFormat.ECDSA_DER; +import static io.getlime.security.powerauth.core.SignatureFormat.ECDSA_JOSE; + +/** + * The {@code SignatureFormat} enumeration defines signature type expected at input, or produced at output. + */ +@Retention(RetentionPolicy.SOURCE) +@IntDef({DEFAULT, ECDSA_DER, ECDSA_JOSE}) +public @interface SignatureFormat { + /** + * If default signature is used, then `ECDSA_DER` is used for ECDSA signature. The raw bytes are always used for + * HMAC signatures. + */ + int DEFAULT = 0; + /** + * ECDSA signature in DER format is expected at input, or produced at output: + *
+     *  ASN.1 notation:
+     *  ECDSASignature ::= SEQUENCE {
+     *     r   INTEGER,
+     *     s   INTEGER
+     * }
+     * 
+ */ + int ECDSA_DER = 1; + /** + * ECDSA signature in JOSE format is epxpected at input, or produced at output. + */ + int ECDSA_JOSE = 2; +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/SignedData.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/SignedData.java index 4468f104..68e57703 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/SignedData.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/SignedData.java @@ -34,15 +34,22 @@ public class SignedData { */ @SigningDataKey public final int signingKey; + /** + * Format of signature expected at input, or produced at output. + */ + @SignatureFormat + public final int signatureFormat; /** * @param data data protected with signature * @param signature signature calculated for data * @param signingKey Key used to sign data, or will be used for the signature calculation. + * @param signatureFormat Format of signature expected at input, or produced at output. */ - public SignedData(byte[] data, byte[] signature, @SigningDataKey int signingKey) { + public SignedData(byte[] data, byte[] signature, @SigningDataKey int signingKey, @SignatureFormat int signatureFormat) { this.data = data; this.signature = signature; this.signingKey = signingKey; + this.signatureFormat = signatureFormat; } } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java index 85569134..5795e388 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java @@ -29,7 +29,6 @@ import com.google.gson.reflect.TypeToken; import java.nio.charset.StandardCharsets; -import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.locks.ReentrantLock; @@ -1791,7 +1790,7 @@ public boolean verifyServerSignedData(byte[] data, byte[] signature, boolean use // Verify signature final int signingKey = useMasterKey ? SigningDataKey.ECDSA_MASTER_SERVER_KEY : SigningDataKey.ECDSA_PERSONALIZED_KEY; - final SignedData signedData = new SignedData(data, signature, signingKey); + final SignedData signedData = new SignedData(data, signature, signingKey, SignatureFormat.ECDSA_DER); return mSession.verifyServerSignedData(signedData) == ErrorCode.OK; } @@ -1805,7 +1804,20 @@ public boolean verifyServerSignedData(byte[] data, byte[] signature, boolean use */ public @Nullable ICancelable signDataWithDevicePrivateKey(@NonNull final Context context, @NonNull PowerAuthAuthentication authentication, @NonNull final byte[] data, @NonNull final IDataSignatureListener listener) { + return signDataWithDevicePrivateKeyImpl(context, authentication, data, SignatureFormat.ECDSA_DER, listener); + } + /** + * Sign provided data with a private key that is stored in secure vault. + * @param context Context. + * @param authentication Authentication object for vault unlock request. + * @param data Data to be signed. + * @param signatureFormat Format of output signature. + * @param listener Listener with callbacks to signature status. + * @return Async task associated with vault unlock request. + */ + private @Nullable + ICancelable signDataWithDevicePrivateKeyImpl(@NonNull final Context context, @NonNull PowerAuthAuthentication authentication, @NonNull final byte[] data, @SignatureFormat int signatureFormat, @NonNull final IDataSignatureListener listener) { // Fetch vault encryption key using vault unlock request. return this.fetchEncryptedVaultUnlockKey(context, authentication, VaultUnlockReason.SIGN_WITH_DEVICE_PRIVATE_KEY, new IFetchEncryptedVaultUnlockKeyListener() { @Override @@ -1813,7 +1825,7 @@ public void onFetchEncryptedVaultUnlockKeySucceed(String encryptedEncryptionKey) if (encryptedEncryptionKey != null) { // Let's sign the data SignatureUnlockKeys keys = new SignatureUnlockKeys(deviceRelatedKey(context), null, null); - byte[] signature = mSession.signDataWithDevicePrivateKey(encryptedEncryptionKey, keys, data); + byte[] signature = mSession.signDataWithDevicePrivateKey(encryptedEncryptionKey, keys, data, signatureFormat); // Propagate error if (signature != null) { listener.onDataSignedSucceed(signature); @@ -1830,8 +1842,10 @@ public void onFetchEncryptedVaultUnlockKeyFailed(Throwable t) { listener.onDataSignedFailed(t); } }); + } + /** * Change the password using local re-encryption, do not validate old password by calling any endpoint. * @@ -2535,7 +2549,7 @@ public ICancelable signJwtWithDevicePrivateKey(@NonNull Context context, @NonNul final String jwtHeader = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9"; // {"alg":"ES256","typ":"JWT"} final String jwtClaims = serialization.serializeJwtObject(claims); final String jwtHeaderAndClaims = jwtHeader + "." + jwtClaims; - return signDataWithDevicePrivateKey(context, authentication, jwtHeaderAndClaims.getBytes(StandardCharsets.US_ASCII), new IDataSignatureListener() { + return signDataWithDevicePrivateKeyImpl(context, authentication, jwtHeaderAndClaims.getBytes(StandardCharsets.US_ASCII), SignatureFormat.ECDSA_JOSE, new IDataSignatureListener() { @Override public void onDataSignedSucceed(@NonNull byte[] signature) { // Encoded signature diff --git a/proj-xcode/PowerAuthCore/PowerAuthCoreSession.h b/proj-xcode/PowerAuthCore/PowerAuthCoreSession.h index faedbbce..00a9d102 100644 --- a/proj-xcode/PowerAuthCore/PowerAuthCoreSession.h +++ b/proj-xcode/PowerAuthCore/PowerAuthCoreSession.h @@ -457,7 +457,8 @@ */ - (nullable NSData*) signDataWithDevicePrivateKey:(nonnull NSString*)cVaultKey keys:(nonnull PowerAuthCoreSignatureUnlockKeys*)unlockKeys - data:(nonnull NSData*)data; + data:(nonnull NSData*)data + format:(PowerAuthCoreSignatureFormat)format; #pragma mark - External Encryption Key diff --git a/proj-xcode/PowerAuthCore/PowerAuthCoreSession.mm b/proj-xcode/PowerAuthCore/PowerAuthCoreSession.mm index f7435eee..a83097c2 100644 --- a/proj-xcode/PowerAuthCore/PowerAuthCoreSession.mm +++ b/proj-xcode/PowerAuthCore/PowerAuthCoreSession.mm @@ -380,15 +380,17 @@ - (nullable NSData*) deriveCryptographicKeyFromVaultKey:(nonnull NSString*)cVaul - (nullable NSData*) signDataWithDevicePrivateKey:(nonnull NSString*)cVaultKey keys:(nonnull PowerAuthCoreSignatureUnlockKeys*)unlockKeys data:(nonnull NSData*)data + format:(PowerAuthCoreSignatureFormat)format { REQUIRE_READ_ACCESS(); std::string cpp_c_vault_key = cc7::objc::CopyFromNSString(cVaultKey); cc7::ByteArray cpp_data = cc7::objc::CopyFromNSData(data); + auto cpp_format = static_cast(format); SignatureUnlockKeys cpp_keys; PowerAuthCoreSignatureUnlockKeysToStruct(unlockKeys, cpp_keys); cc7::ByteArray cpp_signature; - auto error = _session->signDataWithDevicePrivateKey(cpp_c_vault_key, cpp_keys, cpp_data, cpp_signature); + auto error = _session->signDataWithDevicePrivateKey(cpp_c_vault_key, cpp_keys, cpp_data, cpp_format, cpp_signature); if (error == EC_Ok) { return cc7::objc::CopyToNSData(cpp_signature); } diff --git a/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.h b/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.h index 51998aea..5986040d 100644 --- a/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.h +++ b/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.h @@ -318,12 +318,45 @@ typedef NS_ENUM(int, PowerAuthCoreSigningDataKey) { PowerAuthCoreSigningDataKey_HMAC_Activation = 3 }; +/** + The `PowerAuthCoreSignatureFormat` enumeration defines signature type expected at input, or produced + at output. + */ +typedef NS_ENUM(int, PowerAuthCoreSignatureFormat) { + /** + If used, then `PowerAuthCoreSignatureFormat_ECDSA_DER` is used for ECDSA signature. + For the HMAC signature, the raw bytes is always used. + */ + PowerAuthCoreSignatureFormat_Default = 0, + /** + ECDSA signature in DER format is expected at input, or produced at output: + ``` + // ASN.1 notation: + ECDSASignature ::= SEQUENCE { + r INTEGER, + s INTEGER + } + ``` + */ + PowerAuthCoreSignatureFormat_ECDSA_DER = 1, + /** + ECDSA signature in JOSE format is epxpected at input, or produced at output. + */ + PowerAuthCoreSignatureFormat_ECDSA_JOSE = 2 +}; + /** The PowerAuthCoreSignedData object contains data and signature calculated from data. */ @interface PowerAuthCoreSignedData : NSObject - +/** + A signign key to use. + */ @property (nonatomic, assign) PowerAuthCoreSigningDataKey signingDataKey; +/** + A format of signature expected at input or produced at output. + */ +@property (nonatomic, assign) PowerAuthCoreSignatureFormat signatureFormat; /** A data protected with signature */ diff --git a/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.mm b/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.mm index 1e2ebe8e..e613cc1f 100644 --- a/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.mm +++ b/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.mm @@ -156,6 +156,17 @@ - (void) setSigningDataKey:(PowerAuthCoreSigningDataKey)signingDataKey _signedData.signingKey = static_cast(signingDataKey); } +// Signature type + +- (PowerAuthCoreSignatureFormat) signatureFormat +{ + return static_cast(_signedData.signatureFormat); +} + +- (void) setSignatureFormat:(PowerAuthCoreSignatureFormat)signatureType +{ + _signedData.signatureFormat = static_cast(signatureType); +} // Bytes setters and getters @@ -204,7 +215,7 @@ - (void) setSignatureBase64:(NSString *)signatureBase64 #ifdef DEBUG - (NSString*) description { - return [NSString stringWithFormat:@"", self.dataBase64, self.signatureBase64]; + return [NSString stringWithFormat:@"", _signedData.signingKey, self.dataBase64, self.signatureBase64]; } #endif diff --git a/src/PowerAuth/Session.cpp b/src/PowerAuth/Session.cpp index 47daeaa2..f5693071 100644 --- a/src/PowerAuth/Session.cpp +++ b/src/PowerAuth/Session.cpp @@ -706,7 +706,13 @@ namespace powerAuth } if (nullptr != ec_public_key) { // validate signature - success = crypto::ECDSA_ValidateSignature(data.data, data.signature, ec_public_key); + if (data.signatureFormat == SignedData::ECDSA_JOSE) { + // Convert signature from JOSE to DER first. + success = crypto::ECDSA_ValidateSignature(data.data, crypto::ECDSA_JOSEtoDER(data.signature), ec_public_key); + } else { + // No signature conversion required. + success = crypto::ECDSA_ValidateSignature(data.data, data.signature, ec_public_key); + } // } else { CC7_LOG("Session %p: ServerSig: %s public key is invalid.", this, use_master_server_key ? "Master server" : "Server"); @@ -735,7 +741,7 @@ namespace powerAuth } cc7::ByteArray signing_key; if (app_scope) { - signing_key = cc7::MakeRange(_setup.applicationSecret); + signing_key.readFromBase64String(_setup.applicationSecret); } else { // Unlock transport key protocol::SignatureKeys plain; @@ -904,7 +910,8 @@ namespace powerAuth } ErrorCode Session::signDataWithDevicePrivateKey(const std::string & c_vault_key, const SignatureUnlockKeys & keys, - const cc7::ByteRange & in_data, cc7::ByteArray & out_signature) + const cc7::ByteRange & in_data, SignedData::SignatureFormat out_format, + cc7::ByteArray & out_signature) { LOCK_GUARD(); cc7::ByteArray vault_key; @@ -932,6 +939,13 @@ namespace powerAuth // Signature calculation failed. break; } + if (out_format == SignedData::ECDSA_JOSE) { + out_signature = crypto::ECDSA_DERtoJOSE(out_signature); + if (out_signature.empty()) { + // Conversion to JOSE format failed. + break; + } + } code = EC_Ok; } while (false); diff --git a/src/PowerAuth/crypto/ECC.cpp b/src/PowerAuth/crypto/ECC.cpp index 28fb8276..81a015c2 100644 --- a/src/PowerAuth/crypto/ECC.cpp +++ b/src/PowerAuth/crypto/ECC.cpp @@ -23,6 +23,11 @@ #include +#include "../utils/DataReader.h" +#include "../utils/DataWriter.h" +#include +#include + namespace io { namespace getlime @@ -31,7 +36,8 @@ namespace powerAuth { namespace crypto { - + using namespace io::getlime::powerAuth; + // ------------------------------------------------------------------------------------------- // MARK: - ECC routines - // @@ -207,6 +213,9 @@ namespace crypto signedDataHash.data(), (int)signedDataHash.size(), signature.data(), (int)signature.size(), publicKey); + if (result != 1) { + ERR_print_errors_fp(stdout); + } return result == 1; } @@ -236,7 +245,135 @@ namespace crypto signature.resize(signatureSize); return true; } + + // ------------------------------------------------------------------------------------------- + // MARK: - ECDSA Format - + // + static bool _DecodeAsn1ByteSequence(utils::DataReader & reader, cc7::ByteRange & out_data, size_t & out_size) + { + cc7::byte tmp; + if (!reader.readByte(tmp) || tmp != 0x02) { + // Invalid sequence header + return false; + } + if (!reader.readAsn1Count(out_size)) { + // Invalid size + return false; + } + if (out_size > 33) { + // Too big + return false; + } + return reader.readMemoryRange(out_data, out_size); + } + + cc7::ByteArray ECDSA_DERtoJOSE(const cc7::ByteRange & der_signature) + { + cc7::ByteArray out; + auto reader = utils::DataReader(der_signature); + + cc7::byte tmp; + // Read first byte (sequence) + if (!reader.readByte(tmp) || tmp != 0x30) { + return out; + } + size_t sign_length, r_length, s_length; + cc7::ByteRange R, S; + if (!reader.readAsn1Count(sign_length)) { + return out; + } + // Overall length should match DER length - offset + if (sign_length != der_signature.size() - reader.currentOffset()) { + return out; + } + // Read R. + if (!_DecodeAsn1ByteSequence(reader, R, r_length)) { + return out; + } + // Read S. + if (!_DecodeAsn1ByteSequence(reader, S, s_length)) { + return out; + } + + // Everything looks fine. Now construct JOSE signature. + out.reserve(64); + + // Append R + if (r_length > 32) { + out.append(R.subRangeFrom(r_length - 32)); + } else { + if (r_length < 32) { + out.append(32 - r_length, 0); + } + out.append(R); + } + // Append S + if (s_length > 32) { + out.append(S.subRangeFrom(s_length - 32)); + } else { + if (s_length < 32) { + out.append(32 - s_length, 0); + } + out.append(S); + } + return out; + } + + static cc7::ByteArray _SkipPaddingBytes(const cc7::ByteRange & r) + { + cc7::ByteArray out; + size_t offset = 0, size = r.size(); + while (offset != size) { + if (r[offset] != 0) { + break; + } + ++offset; + } + // If the encoded number is negative, then keep zero byte as prefix. + if (r[offset] > 0x7F) { + if (offset == 0) { + // We're already at the beginning of range, so prepend zero before the sequence + out.push_back(0); + } else { + // Offset is greater than 0, so we can copy zero from the padding + offset--; + } + } + out.append(r.subRangeFrom(offset)); + return out; + } + + static cc7::ByteArray _EncodeAsn1ByteSequence(utils::DataWriter & writer, const cc7::ByteRange & bytes) + { + writer.reset(); + writer.writeByte(0x02); + writer.writeAsn1Count(bytes.size()); + writer.writeMemory(bytes); + return writer.serializedData(); + } + + cc7::ByteArray ECDSA_JOSEtoDER(const cc7::ByteRange & jose_signature) + { + if (jose_signature.size() != 64) { + return cc7::ByteArray(); + } + // Split input data into half and skip zero leading bytes for each parameter. + auto R = _SkipPaddingBytes(jose_signature.subRangeTo(32)); + auto S = _SkipPaddingBytes(jose_signature.subRangeFrom(32)); + + auto writer = utils::DataWriter(); + auto encoded_R = _EncodeAsn1ByteSequence(writer, R); + auto encoded_S = _EncodeAsn1ByteSequence(writer, S); + // Encode the whole sequence + writer.reset(); + writer.writeByte(0x30); + writer.writeAsn1Count(encoded_R.size() + encoded_S.size()); + writer.writeMemory(encoded_R); + writer.writeMemory(encoded_S); + return writer.serializedData(); + } + // ------------------------------------------------------------------------------------------- // MARK: - ECDH - // diff --git a/src/PowerAuth/crypto/ECC.h b/src/PowerAuth/crypto/ECC.h index 4e8382fc..726e4e1d 100644 --- a/src/PowerAuth/crypto/ECC.h +++ b/src/PowerAuth/crypto/ECC.h @@ -90,6 +90,15 @@ namespace crypto Computes signature for data with given private key. */ bool ECDSA_ComputeSignature(const cc7::ByteRange & data, EC_KEY * privateKey, cc7::ByteArray & signature); + + /** + Convert ECDSA signature from DER format to JOSE. If operation fails, then returned array is empty. + */ + cc7::ByteArray ECDSA_DERtoJOSE(const cc7::ByteRange & der_signature); + /** + Convert ECDSA signature from JOSE to DER format. If operation fails, then returned array is empty. + */ + cc7::ByteArray ECDSA_JOSEtoDER(const cc7::ByteRange & jose_signature); // ------------------------------------------------------------------------------------------- // MARK: - ECDH - diff --git a/src/PowerAuth/jni/SessionJNI.cpp b/src/PowerAuth/jni/SessionJNI.cpp index c19cd9fb..407e1620 100644 --- a/src/PowerAuth/jni/SessionJNI.cpp +++ b/src/PowerAuth/jni/SessionJNI.cpp @@ -461,13 +461,15 @@ CC7_JNI_METHOD_PARAMS(jint, verifyServerSignedData, jobject signedData) } // Load parameters into C++ objects jclass requestClazz = CC7_JNI_MODULE_FIND_CLASS("SignedData"); - // Get type of key + // Get enum types jint signingKey = CC7_JNI_GET_FIELD_INT(signedData, requestClazz, "signingKey"); + jint signatureFormat = CC7_JNI_GET_FIELD_INT(signedData, requestClazz, "signatureFormat"); // Prepare cpp structure SignedData cppSignedData; - cppSignedData.signingKey = static_cast(signingKey); - cppSignedData.data = cc7::jni::CopyFromJavaByteArray(env, CC7_JNI_GET_FIELD_BYTEARRAY(signedData, requestClazz, "data")); - cppSignedData.signature = cc7::jni::CopyFromJavaByteArray(env, CC7_JNI_GET_FIELD_BYTEARRAY(signedData, requestClazz, "signature")); + cppSignedData.signingKey = static_cast(signingKey); + cppSignedData.signatureFormat = static_cast(signatureFormat); + cppSignedData.data = cc7::jni::CopyFromJavaByteArray(env, CC7_JNI_GET_FIELD_BYTEARRAY(signedData, requestClazz, "data")); + cppSignedData.signature = cc7::jni::CopyFromJavaByteArray(env, CC7_JNI_GET_FIELD_BYTEARRAY(signedData, requestClazz, "signature")); return (jint) session->verifyServerSignedData(cppSignedData); } @@ -483,7 +485,6 @@ CC7_JNI_METHOD_PARAMS(jint, signDataWithHmacKey, jobject dataToSign, jobject unl } // Load parameters into C++ objects SignatureUnlockKeys cppUnlockKeys; - SignedData cppDataToSign; if (unlockKeys != nullptr) { if (false == LoadSignatureUnlockKeys(cppUnlockKeys, env, unlockKeys)) { return EC_WrongParam; @@ -493,8 +494,12 @@ CC7_JNI_METHOD_PARAMS(jint, signDataWithHmacKey, jobject dataToSign, jobject unl jclass requestClazz = CC7_JNI_MODULE_FIND_CLASS("SignedData"); // Get type of key jint signingKey = CC7_JNI_GET_FIELD_INT(dataToSign, requestClazz, "signingKey"); - cppDataToSign.signingKey = static_cast(signingKey); - cppDataToSign.data = cc7::jni::CopyFromJavaByteArray(env, CC7_JNI_GET_FIELD_BYTEARRAY(dataToSign, requestClazz, "data")); + jint signatureFormat = CC7_JNI_GET_FIELD_INT(dataToSign, requestClazz, "signatureFormat"); + // Prepare cpp structure + SignedData cppDataToSign; + cppDataToSign.signingKey = static_cast(signingKey); + cppDataToSign.signatureFormat = static_cast(signatureFormat); + cppDataToSign.data = cc7::jni::CopyFromJavaByteArray(env, CC7_JNI_GET_FIELD_BYTEARRAY(dataToSign, requestClazz, "data")); // Call session auto ec = session->signDataWithHmacKey(cppDataToSign, cppUnlockKeys); if (ec == EC_Ok) { @@ -607,9 +612,9 @@ CC7_JNI_METHOD_PARAMS(jbyteArray, deriveCryptographicKeyFromVaultKey, jstring cV } // -// public native byte[] signDataWithDevicePrivateKey(String cVaultKey, SignatureUnlockKeys unlockKeys, byte[] data); +// public native byte[] signDataWithDevicePrivateKey(String cVaultKey, SignatureUnlockKeys unlockKeys, byte[] data, int signatureFormat); // -CC7_JNI_METHOD_PARAMS(jbyteArray, signDataWithDevicePrivateKey, jstring cVaultKey, jobject unlockKeys, jbyteArray data) +CC7_JNI_METHOD_PARAMS(jbyteArray, signDataWithDevicePrivateKey, jstring cVaultKey, jobject unlockKeys, jbyteArray data, jint signatureFormat) { auto session = CC7_THIS_OBJ(); if (!session || !cVaultKey || !unlockKeys || !data) { @@ -619,12 +624,13 @@ CC7_JNI_METHOD_PARAMS(jbyteArray, signDataWithDevicePrivateKey, jstring cVaultKe // Load parameters into C++ objects std::string cppCVaultKey = cc7::jni::CopyFromJavaString(env, cVaultKey); cc7::ByteArray cppData = cc7::jni::CopyFromJavaByteArray(env, data); + auto cppSignatureFormat = static_cast(signatureFormat); SignatureUnlockKeys cppUnlockKeys; if (false == LoadSignatureUnlockKeys(cppUnlockKeys, env, unlockKeys)) { return NULL; } cc7::ByteArray signature; - ErrorCode code = session->signDataWithDevicePrivateKey(cppCVaultKey, cppUnlockKeys, cppData, signature); + ErrorCode code = session->signDataWithDevicePrivateKey(cppCVaultKey, cppUnlockKeys, cppData, cppSignatureFormat, signature); if (code != EC_Ok) { return NULL; } diff --git a/src/PowerAuth/utils/DataReader.cpp b/src/PowerAuth/utils/DataReader.cpp index 02d7b877..bf051658 100644 --- a/src/PowerAuth/utils/DataReader.cpp +++ b/src/PowerAuth/utils/DataReader.cpp @@ -240,6 +240,38 @@ namespace utils } return true; } + + bool DataReader::readAsn1Count(size_t &out_value) + { + byte tmp[4] = { 0, 0, 0, 0 }; + if (!readByte(tmp[0])) { + return false; + } + if (!(tmp[0] & 0x80)) { + // One byte with length lesser than 0x80 + out_value = tmp[0]; + // + } else { + // Length is encoded in multiple bytes. The first byte determines the length + // of encoded length. Bit 7 is set to 1. + size_t llength = tmp[0] & 0x7F; + tmp[0] = 0; + if (llength > 4 || !llength) { + // Too long, we support up to 4 bytes length. Other invalid + // value is zero. + return false; + } + size_t offset = 4 - llength; + if (!readRawMemory(tmp + offset, llength)) { + return false; + } + out_value = (size_t(tmp[0]) << 24) | + (size_t(tmp[1]) << 16) | + (size_t(tmp[2]) << 8 ) | + size_t(tmp[3]); + } + return true; + } // Data versioning diff --git a/src/PowerAuth/utils/DataReader.h b/src/PowerAuth/utils/DataReader.h index ebc1ec9b..6c746a68 100644 --- a/src/PowerAuth/utils/DataReader.h +++ b/src/PowerAuth/utils/DataReader.h @@ -140,10 +140,15 @@ namespace utils bool readU64(cc7::U64 & out_value); /** - Returns count from data stream. This is complementary method + Reads count from data stream. This is complementary method to DataWriter::writeCount(). */ bool readCount(size_t & out_value); + /** + Reads count in ASN.1 format from the data stream. This is cimplementary method + to DataWriter::writeAsn1Count(). + */ + bool readAsn1Count(size_t & out_value); // Data versioning diff --git a/src/PowerAuth/utils/DataWriter.cpp b/src/PowerAuth/utils/DataWriter.cpp index 8ae68182..7399815d 100644 --- a/src/PowerAuth/utils/DataWriter.cpp +++ b/src/PowerAuth/utils/DataWriter.cpp @@ -135,6 +135,35 @@ namespace utils } return true; } + + bool DataWriter::writeAsn1Count(size_t n) + { + if (n <= 0x7F) { + // + writeByte(n); + // + } else if (n <= 0xFF) { + // + writeByte(0x81); + writeByte(n); + // + } else if (n <= 0xFFFF) { + // + writeByte(0x82); + writeU16(n); + // + } else if (n <= 0x3FFFFFFF) { + // + writeByte(0x84); + writeU32((cc7::U32)n); + // + } else { + // ASN.1 supports even bigger numbers, but it's overkill for our purpose + CC7_ASSERT(false, "Count is too big."); + return false; + } + return true; + } size_t DataWriter::maxCount() { diff --git a/src/PowerAuth/utils/DataWriter.h b/src/PowerAuth/utils/DataWriter.h index c996b6b2..2c0f8f62 100644 --- a/src/PowerAuth/utils/DataWriter.h +++ b/src/PowerAuth/utils/DataWriter.h @@ -105,6 +105,11 @@ namespace utils */ bool writeCount(size_t count); + /** + Write a count to the stream in ASN.1 binary format. + */ + bool writeAsn1Count(size_t count); + /** Returns maximum supported value which can be serialized as a counter. The returned value is the same for all supported diff --git a/src/PowerAuthTests/pa2CryptoECDSATests.cpp b/src/PowerAuthTests/pa2CryptoECDSATests.cpp index cb932e4e..21df91a5 100644 --- a/src/PowerAuthTests/pa2CryptoECDSATests.cpp +++ b/src/PowerAuthTests/pa2CryptoECDSATests.cpp @@ -65,6 +65,12 @@ namespace powerAuthTests ccstAssertTrue(success); ccstAssertFalse(signature.empty()); + // convert to JOSE and back to DER + auto jose_signature = crypto::ECDSA_DERtoJOSE(signature); + ccstAssertFalse(signature.empty()); + auto der_signature = crypto::ECDSA_JOSEtoDER(jose_signature); + ccstAssertEqual(signature, der_signature); + // Validate signature auto result = crypto::ECDSA_ValidateSignature(message, signature, public_key); ccstAssertTrue(result); diff --git a/src/PowerAuthTests/pa2DataWriterReaderTests.cpp b/src/PowerAuthTests/pa2DataWriterReaderTests.cpp index 3affeb6d..1db0e43c 100644 --- a/src/PowerAuthTests/pa2DataWriterReaderTests.cpp +++ b/src/PowerAuthTests/pa2DataWriterReaderTests.cpp @@ -36,6 +36,7 @@ namespace powerAuthTests pa2DataWriterReaderTests() { CC7_REGISTER_TEST_METHOD(testReadWriteCount) + CC7_REGISTER_TEST_METHOD(testReadWriteAsn1Count) CC7_REGISTER_TEST_METHOD(testReadWriteMethods) CC7_REGISTER_TEST_METHOD(testNotEnoughData) CC7_REGISTER_TEST_METHOD(testVersions) @@ -81,6 +82,57 @@ namespace powerAuthTests ccstAssertTrue(simulated_failure_passed, "Max value not tested properly."); } + /* + The test validates writeAsn1Count() / readAsn1Count() functionality + */ + void testReadWriteAsn1Count() + { + size_t test_value = 1; + size_t restored_value; + bool simulated_failure_passed = false; + while (test_value <= ((size_t)-1)/2) { + + DataWriter writer; + bool write_result = writer.writeAsn1Count(test_value); + DataReader reader(writer.serializedData()); + bool read_result = reader.readAsn1Count(restored_value); + + if (test_value <= DataWriter::maxCount()) { + // Values should be correct + ccstAssertTrue(write_result); + ccstAssertTrue(read_result); + ccstAssertEqual(reader.remainingSize(), 0); + ccstAssertEqual(test_value, restored_value, "Restored: 0x%x, Expected 0x%x", restored_value, test_value); + } else { + ccstAssertFalse(write_result); + // read result is not important + simulated_failure_passed = true; + } + + // Calculate next test value + if (test_value & 1) { + test_value += 1; + } else { + test_value = (test_value << 1) - 1; + } + } + // There must be at least one pass when write_result is false. + ccstAssertTrue(simulated_failure_passed, "Max value not tested properly."); + // Now test a special cases + { + // Length encoded in 3 bytes + DataWriter writer; + writer.writeByte(0x83); + writer.writeByte(0x10); + writer.writeByte(0x3E); + writer.writeByte(0x8F); + DataReader reader(writer.serializedData()); + size_t size; + ccstAssertTrue(reader.readAsn1Count(size)); + ccstAssertEqual(0x103E8F, size); + } + } + void readWriteSequenceTest(ByteArray * arr) { if (arr) { diff --git a/src/PowerAuthTests/pa2SessionTests.cpp b/src/PowerAuthTests/pa2SessionTests.cpp index f805e0ae..7af61ade 100644 --- a/src/PowerAuthTests/pa2SessionTests.cpp +++ b/src/PowerAuthTests/pa2SessionTests.cpp @@ -834,7 +834,7 @@ namespace powerAuthTests keys.possessionUnlockKey = possessionUnlock; cc7::ByteArray signature; - ec = s1.signDataWithDevicePrivateKey(cVaultKey, keys, cc7::MakeRange("Hello World!"), signature); + ec = s1.signDataWithDevicePrivateKey(cVaultKey, keys, cc7::MakeRange("Hello World!"), SignedData::ECDSA_DER, signature); ccstAssertEqual(ec, EC_Ok); ccstAssertTrue(!signature.empty()); // Validate signature...