diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 612bb96be..c75d06f08 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ permissions: contents: read env: - BORINGSSL_VERSION: 0.20260813.0 + BORINGSSL_VERSION: 0.20260929.0 jobs: boringssl_version_check: diff --git a/android-stub/src/main/java/android/compat/Compatibility.java b/android-stub/src/main/java/android/compat/Compatibility.java new file mode 100644 index 000000000..6aa817f2b --- /dev/null +++ b/android-stub/src/main/java/android/compat/Compatibility.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * 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 android.compat; + +@SuppressWarnings({"unused", "DoNotCallSuggester"}) +public final class Compatibility { + private Compatibility() {} + + public static boolean isChangeEnabled(long changeId) { + throw new RuntimeException("Stub!"); + } +} diff --git a/android-stub/src/main/java/android/compat/annotation/ChangeId.java b/android-stub/src/main/java/android/compat/annotation/ChangeId.java new file mode 100644 index 000000000..3aadf2e06 --- /dev/null +++ b/android-stub/src/main/java/android/compat/annotation/ChangeId.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * 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 android.compat.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.FIELD}) +public @interface ChangeId {} diff --git a/android-stub/src/main/java/javax/crypto/DecapsulateException.java b/android-stub/src/main/java/javax/crypto/DecapsulateException.java new file mode 100644 index 000000000..9ac9320f5 --- /dev/null +++ b/android-stub/src/main/java/javax/crypto/DecapsulateException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * 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 javax.crypto; + +import java.security.GeneralSecurityException; + +@SuppressWarnings({"unused", "DoNotCallSuggester"}) +public class DecapsulateException extends GeneralSecurityException { + public DecapsulateException(String message) { + throw new UnsupportedOperationException("Stub!"); + } + + public DecapsulateException(String message, Throwable cause) { + throw new UnsupportedOperationException("Stub!"); + } +} diff --git a/android-stub/src/main/java/javax/crypto/KDF.java b/android-stub/src/main/java/javax/crypto/KDF.java new file mode 100644 index 000000000..8f8884855 --- /dev/null +++ b/android-stub/src/main/java/javax/crypto/KDF.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * 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 javax.crypto; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Provider; +import java.security.spec.AlgorithmParameterSpec; + +@SuppressWarnings({"unused", "DoNotCallSuggester"}) +public final class KDF { + private KDF() {} + + public static KDF getInstance(String algorithm) throws NoSuchAlgorithmException { + throw new UnsupportedOperationException("Stub!"); + } + + public static KDF getInstance(String algorithm, KDFParameters kdfParameters) + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException { + throw new UnsupportedOperationException("Stub!"); + } + + public static KDF getInstance(String algorithm, Provider provider) + throws NoSuchAlgorithmException { + throw new UnsupportedOperationException("Stub!"); + } + + public static KDF getInstance(String algorithm, KDFParameters kdfParameters, Provider provider) + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException { + throw new UnsupportedOperationException("Stub!"); + } + + public static KDF getInstance(String algorithm, String provider) + throws NoSuchAlgorithmException, NoSuchProviderException { + throw new UnsupportedOperationException("Stub!"); + } + + public static KDF getInstance(String algorithm, KDFParameters kdfParameters, String provider) + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, + NoSuchProviderException { + throw new UnsupportedOperationException("Stub!"); + } + + public String getAlgorithm() { + throw new UnsupportedOperationException("Stub!"); + } + + public Provider getProvider() { + throw new UnsupportedOperationException("Stub!"); + } + + public KDFParameters getParameters() { + throw new UnsupportedOperationException("Stub!"); + } + + public SecretKey deriveKey(String alg, AlgorithmParameterSpec derivationSpec) + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException { + throw new UnsupportedOperationException("Stub!"); + } + + public byte[] deriveData(AlgorithmParameterSpec derivationSpec) + throws InvalidAlgorithmParameterException { + throw new UnsupportedOperationException("Stub!"); + } +} diff --git a/android-stub/src/main/java/javax/crypto/KDFParameters.java b/android-stub/src/main/java/javax/crypto/KDFParameters.java new file mode 100644 index 000000000..a3732e320 --- /dev/null +++ b/android-stub/src/main/java/javax/crypto/KDFParameters.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * 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 javax.crypto; + +public interface KDFParameters {} diff --git a/android-stub/src/main/java/javax/crypto/KDFSpi.java b/android-stub/src/main/java/javax/crypto/KDFSpi.java new file mode 100644 index 000000000..fd11bd1ac --- /dev/null +++ b/android-stub/src/main/java/javax/crypto/KDFSpi.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * 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 javax.crypto; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.AlgorithmParameterSpec; + +@SuppressWarnings({"unused", "DoNotCallSuggester"}) +public abstract class KDFSpi { + protected KDFSpi(KDFParameters kdfParameters) throws InvalidAlgorithmParameterException {} + + protected abstract KDFParameters engineGetParameters(); + + protected abstract SecretKey engineDeriveKey(String alg, AlgorithmParameterSpec derivationSpec) + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException; + + protected abstract byte[] engineDeriveData(AlgorithmParameterSpec derivationSpec) + throws InvalidAlgorithmParameterException; +} diff --git a/android-stub/src/main/java/javax/crypto/KEM.java b/android-stub/src/main/java/javax/crypto/KEM.java new file mode 100644 index 000000000..f3e5da5ba --- /dev/null +++ b/android-stub/src/main/java/javax/crypto/KEM.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * 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 javax.crypto; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; + +@SuppressWarnings({"unused", "DoNotCallSuggester"}) +public final class KEM { + public static final class Encapsulated { + public Encapsulated(SecretKey key, byte[] encapsulation, byte[] params) { + throw new UnsupportedOperationException("Stub!"); + } + + public SecretKey key() { + throw new UnsupportedOperationException("Stub!"); + } + + public byte[] encapsulation() { + throw new UnsupportedOperationException("Stub!"); + } + + public byte[] params() { + throw new UnsupportedOperationException("Stub!"); + } + } + + public static final class Encapsulator { + private Encapsulator() {} + + public String providerName() { + throw new UnsupportedOperationException("Stub!"); + } + + public Encapsulated encapsulate() { + throw new UnsupportedOperationException("Stub!"); + } + + public Encapsulated encapsulate(int from, int to, String algorithm) { + throw new UnsupportedOperationException("Stub!"); + } + + public int secretSize() { + throw new UnsupportedOperationException("Stub!"); + } + + public int encapsulationSize() { + throw new UnsupportedOperationException("Stub!"); + } + } + + public static final class Decapsulator { + private Decapsulator() {} + + public String providerName() { + throw new UnsupportedOperationException("Stub!"); + } + + public SecretKey decapsulate(byte[] encapsulation) throws DecapsulateException { + throw new UnsupportedOperationException("Stub!"); + } + + public SecretKey decapsulate(byte[] encapsulation, int from, int to, String algorithm) + throws DecapsulateException { + throw new UnsupportedOperationException("Stub!"); + } + + public int secretSize() { + throw new UnsupportedOperationException("Stub!"); + } + + public int encapsulationSize() { + throw new UnsupportedOperationException("Stub!"); + } + } + + private KEM() {} + + public static KEM getInstance(String algorithm) throws NoSuchAlgorithmException { + throw new UnsupportedOperationException("Stub!"); + } + + public static KEM getInstance(String algorithm, Provider provider) + throws NoSuchAlgorithmException { + throw new UnsupportedOperationException("Stub!"); + } + + public static KEM getInstance(String algorithm, String provider) + throws NoSuchAlgorithmException, NoSuchProviderException { + throw new UnsupportedOperationException("Stub!"); + } + + public String getAlgorithm() { + throw new UnsupportedOperationException("Stub!"); + } + + public Provider getProvider() { + throw new UnsupportedOperationException("Stub!"); + } + + public Encapsulator newEncapsulator(PublicKey publicKey) throws InvalidKeyException { + throw new UnsupportedOperationException("Stub!"); + } + + public Encapsulator newEncapsulator(PublicKey publicKey, SecureRandom secureRandom) + throws InvalidKeyException { + throw new UnsupportedOperationException("Stub!"); + } + + public Encapsulator newEncapsulator(PublicKey publicKey, AlgorithmParameterSpec spec, + SecureRandom secureRandom) + throws InvalidAlgorithmParameterException, InvalidKeyException { + throw new UnsupportedOperationException("Stub!"); + } + + public Decapsulator newDecapsulator(PrivateKey privateKey) throws InvalidKeyException { + throw new UnsupportedOperationException("Stub!"); + } + + public Decapsulator newDecapsulator(PrivateKey privateKey, AlgorithmParameterSpec spec) + throws InvalidAlgorithmParameterException, InvalidKeyException { + throw new UnsupportedOperationException("Stub!"); + } +} diff --git a/android-stub/src/main/java/javax/crypto/KEMSpi.java b/android-stub/src/main/java/javax/crypto/KEMSpi.java new file mode 100644 index 000000000..0a2dca567 --- /dev/null +++ b/android-stub/src/main/java/javax/crypto/KEMSpi.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * 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 javax.crypto; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; + +@SuppressWarnings({"unused", "DoNotCallSuggester"}) +public interface KEMSpi { + interface EncapsulatorSpi { + KEM.Encapsulated engineEncapsulate(int from, int to, String algorithm); + int engineSecretSize(); + int engineEncapsulationSize(); + } + + interface DecapsulatorSpi { + SecretKey engineDecapsulate(byte[] encapsulation, int from, int to, String algorithm) + throws DecapsulateException; + int engineSecretSize(); + int engineEncapsulationSize(); + } + + EncapsulatorSpi engineNewEncapsulator(PublicKey publicKey, AlgorithmParameterSpec spec, + SecureRandom secureRandom) + throws InvalidAlgorithmParameterException, InvalidKeyException; + + DecapsulatorSpi engineNewDecapsulator(PrivateKey privateKey, AlgorithmParameterSpec spec) + throws InvalidAlgorithmParameterException, InvalidKeyException; +} diff --git a/android-stub/src/main/java/javax/crypto/spec/HKDFParameterSpec.java b/android-stub/src/main/java/javax/crypto/spec/HKDFParameterSpec.java new file mode 100644 index 000000000..482bd25d3 --- /dev/null +++ b/android-stub/src/main/java/javax/crypto/spec/HKDFParameterSpec.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * 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 javax.crypto.spec; + +import java.security.spec.AlgorithmParameterSpec; +import java.util.List; + +import javax.crypto.SecretKey; + +@SuppressWarnings({"unused", "DoNotCallSuggester"}) +public interface HKDFParameterSpec extends AlgorithmParameterSpec { + final class Builder { + private Builder() {} + + public Extract extractOnly() { + throw new UnsupportedOperationException("Stub!"); + } + + public ExtractThenExpand thenExpand(byte[] info, int length) { + throw new UnsupportedOperationException("Stub!"); + } + + public Builder addIKM(SecretKey ikm) { + throw new UnsupportedOperationException("Stub!"); + } + + public Builder addIKM(byte[] ikm) { + throw new UnsupportedOperationException("Stub!"); + } + + public Builder addSalt(SecretKey salt) { + throw new UnsupportedOperationException("Stub!"); + } + + public Builder addSalt(byte[] salt) { + throw new UnsupportedOperationException("Stub!"); + } + } + + static Builder ofExtract() { + throw new UnsupportedOperationException("Stub!"); + } + + static Expand expandOnly(SecretKey prk, byte[] info, int length) { + throw new UnsupportedOperationException("Stub!"); + } + + final class Extract implements HKDFParameterSpec { + private Extract() {} + + public List ikms() { + throw new UnsupportedOperationException("Stub!"); + } + + public List salts() { + throw new UnsupportedOperationException("Stub!"); + } + } + + final class Expand implements HKDFParameterSpec { + private Expand() {} + + public SecretKey prk() { + throw new UnsupportedOperationException("Stub!"); + } + + public byte[] info() { + throw new UnsupportedOperationException("Stub!"); + } + + public int length() { + throw new UnsupportedOperationException("Stub!"); + } + } + + final class ExtractThenExpand implements HKDFParameterSpec { + private ExtractThenExpand() {} + + public List ikms() { + throw new UnsupportedOperationException("Stub!"); + } + + public List salts() { + throw new UnsupportedOperationException("Stub!"); + } + + public byte[] info() { + throw new UnsupportedOperationException("Stub!"); + } + + public int length() { + throw new UnsupportedOperationException("Stub!"); + } + } +} diff --git a/android/src/main/java/org/conscrypt/Platform.java b/android/src/main/java/org/conscrypt/Platform.java index 248f82576..76176ca90 100644 --- a/android/src/main/java/org/conscrypt/Platform.java +++ b/android/src/main/java/org/conscrypt/Platform.java @@ -882,6 +882,28 @@ public static boolean isPakeSupported() { return false; } + public static boolean isPqcKemKdfSupported() { + return false; + } + + /** + * Returns true if PQC key exchange (X25519MLKEM768) should be enabled by default. + * + *

{@code android.compat.Compatibility.isChangeEnabled(long)} is {@code @IntraCoreApi} / + * {@code @SystemApi(client = MODULE_LIBRARIES)} and is blocked by hiddenapi restrictions for + * unbundled Conscrypt running in regular application classloaders, so per-app compat gating is + * only performed in the platform module ({@code platform/src/main/java/org/conscrypt/Platform.java}). + */ + private static volatile boolean pqcDefaultEnabled = true; + + public static boolean isPqcDefaultEnabled() { + return pqcDefaultEnabled; + } + + public static void setPqcDefaultEnabledForTesting(boolean enabled) { + pqcDefaultEnabled = enabled; + } + public static boolean isSdkGreater(int sdk) { return Build.VERSION.SDK_INT > sdk; } diff --git a/common/src/jni/main/cpp/conscrypt/native_crypto.cc b/common/src/jni/main/cpp/conscrypt/native_crypto.cc index 60a3760cf..4dadabdeb 100644 --- a/common/src/jni/main/cpp/conscrypt/native_crypto.cc +++ b/common/src/jni/main/cpp/conscrypt/native_crypto.cc @@ -3967,6 +3967,272 @@ static jbyteArray NativeCrypto_MLKEM1024_public_key_from_seed(JNIEnv* env, jclas return publicKeyRef.release(); } +static void NativeCrypto_MLKEM768_encap(JNIEnv* env, jclass, jbyteArray publicKeyBytes, + jbyteArray outCiphertext, jbyteArray outSharedSecret) { + CHECK_ERROR_QUEUE_ON_RETURN; + JNI_TRACE("MLKEM768_encap(%p, %p, %p)", publicKeyBytes, outCiphertext, outSharedSecret); + + if (publicKeyBytes == nullptr) { + conscrypt::jniutil::throwNullPointerException(env, "publicKeyBytes == null"); + return; + } + if (outCiphertext == nullptr) { + conscrypt::jniutil::throwNullPointerException(env, "outCiphertext == null"); + return; + } + if (outSharedSecret == nullptr) { + conscrypt::jniutil::throwNullPointerException(env, "outSharedSecret == null"); + return; + } + + ScopedByteArrayRO pkArray(env, publicKeyBytes); + if (pkArray.get() == nullptr) { + conscrypt::jniutil::throwOutOfMemory(env, "Unable to allocate buffer for pkArray"); + return; + } + if (pkArray.size() != MLKEM768_PUBLIC_KEY_BYTES) { + conscrypt::jniutil::throwIllegalArgumentException(env, "publicKeyBytes length != 1184"); + return; + } + + ScopedByteArrayRW ctArray(env, outCiphertext); + if (ctArray.get() == nullptr) { + conscrypt::jniutil::throwOutOfMemory(env, "Unable to allocate buffer for ctArray"); + return; + } + if (ctArray.size() != MLKEM768_CIPHERTEXT_BYTES) { + conscrypt::jniutil::throwIllegalArgumentException(env, "outCiphertext length != 1088"); + return; + } + + ScopedByteArrayRW ssArray(env, outSharedSecret); + if (ssArray.get() == nullptr) { + conscrypt::jniutil::throwOutOfMemory(env, "Unable to allocate buffer for ssArray"); + return; + } + if (ssArray.size() != MLKEM_SHARED_SECRET_BYTES) { + conscrypt::jniutil::throwIllegalArgumentException(env, "outSharedSecret length != 32"); + return; + } + + CBS cbs; + CBS_init(&cbs, reinterpret_cast(pkArray.get()), pkArray.size()); + MLKEM768_public_key publicKey; + if (!MLKEM768_parse_public_key(&publicKey, &cbs) || CBS_len(&cbs) != 0) { + JNI_TRACE("MLKEM768_parse_public_key failed"); + ERR_clear_error(); + conscrypt::jniutil::throwIllegalArgumentException(env, "MLKEM768_parse_public_key failed"); + return; + } + + MLKEM768_encap(reinterpret_cast(ctArray.get()), + reinterpret_cast(ssArray.get()), &publicKey); + JNI_TRACE("MLKEM768_encap(%p, %p, %p) => success", publicKeyBytes, outCiphertext, + outSharedSecret); +} + +static jbyteArray NativeCrypto_MLKEM768_decap(JNIEnv* env, jclass, jbyteArray ciphertext, + jbyteArray privateKeySeed) { + CHECK_ERROR_QUEUE_ON_RETURN; + JNI_TRACE("MLKEM768_decap(%p, %p)", ciphertext, privateKeySeed); + + if (ciphertext == nullptr) { + conscrypt::jniutil::throwNullPointerException(env, "ciphertext == null"); + return nullptr; + } + if (privateKeySeed == nullptr) { + conscrypt::jniutil::throwNullPointerException(env, "privateKeySeed == null"); + return nullptr; + } + + ScopedByteArrayRO ctArray(env, ciphertext); + if (ctArray.get() == nullptr) { + conscrypt::jniutil::throwOutOfMemory(env, "Unable to allocate buffer for ctArray"); + return nullptr; + } + if (ctArray.size() != MLKEM768_CIPHERTEXT_BYTES) { + conscrypt::jniutil::throwIllegalArgumentException(env, "ciphertext length != 1088"); + return nullptr; + } + + ScopedByteArrayRO seedArray(env, privateKeySeed); + if (seedArray.get() == nullptr) { + conscrypt::jniutil::throwOutOfMemory(env, "Unable to allocate buffer for seedArray"); + return nullptr; + } + if (seedArray.size() != MLKEM_SEED_BYTES) { + conscrypt::jniutil::throwIllegalArgumentException(env, "privateKeySeed length != 64"); + return nullptr; + } + + MLKEM768_private_key privateKey; + if (!MLKEM768_private_key_from_seed( + &privateKey, reinterpret_cast(seedArray.get()), seedArray.size())) { + JNI_TRACE("MLKEM768_private_key_from_seed failed"); + ERR_clear_error(); + conscrypt::jniutil::throwIllegalArgumentException(env, + "MLKEM768_private_key_from_seed failed"); + return nullptr; + } + + ScopedLocalRef ssRef( + env, env->NewByteArray(static_cast(MLKEM_SHARED_SECRET_BYTES))); + if (ssRef.get() == nullptr) { + conscrypt::jniutil::throwOutOfMemory(env, "Unable to allocate buffer for ssRef"); + return nullptr; + } + ScopedByteArrayRW ssArray(env, ssRef.get()); + if (ssArray.get() == nullptr) { + conscrypt::jniutil::throwOutOfMemory(env, "Unable to allocate buffer for ssArray"); + return nullptr; + } + + if (!MLKEM768_decap(reinterpret_cast(ssArray.get()), + reinterpret_cast(ctArray.get()), ctArray.size(), + &privateKey)) { + JNI_TRACE("MLKEM768_decap failed"); + ERR_clear_error(); + conscrypt::jniutil::throwIllegalStateException(env, "MLKEM768_decap failed"); + return nullptr; + } + + JNI_TRACE("MLKEM768_decap(%p, %p) => %p", ciphertext, privateKeySeed, ssRef.get()); + return ssRef.release(); +} + +static void NativeCrypto_MLKEM1024_encap(JNIEnv* env, jclass, jbyteArray publicKeyBytes, + jbyteArray outCiphertext, jbyteArray outSharedSecret) { + CHECK_ERROR_QUEUE_ON_RETURN; + JNI_TRACE("MLKEM1024_encap(%p, %p, %p)", publicKeyBytes, outCiphertext, outSharedSecret); + + if (publicKeyBytes == nullptr) { + conscrypt::jniutil::throwNullPointerException(env, "publicKeyBytes == null"); + return; + } + if (outCiphertext == nullptr) { + conscrypt::jniutil::throwNullPointerException(env, "outCiphertext == null"); + return; + } + if (outSharedSecret == nullptr) { + conscrypt::jniutil::throwNullPointerException(env, "outSharedSecret == null"); + return; + } + + ScopedByteArrayRO pkArray(env, publicKeyBytes); + if (pkArray.get() == nullptr) { + conscrypt::jniutil::throwOutOfMemory(env, "Unable to allocate buffer for pkArray"); + return; + } + if (pkArray.size() != MLKEM1024_PUBLIC_KEY_BYTES) { + conscrypt::jniutil::throwIllegalArgumentException(env, "publicKeyBytes length != 1568"); + return; + } + + ScopedByteArrayRW ctArray(env, outCiphertext); + if (ctArray.get() == nullptr) { + conscrypt::jniutil::throwOutOfMemory(env, "Unable to allocate buffer for ctArray"); + return; + } + if (ctArray.size() != MLKEM1024_CIPHERTEXT_BYTES) { + conscrypt::jniutil::throwIllegalArgumentException(env, "outCiphertext length != 1568"); + return; + } + + ScopedByteArrayRW ssArray(env, outSharedSecret); + if (ssArray.get() == nullptr) { + conscrypt::jniutil::throwOutOfMemory(env, "Unable to allocate buffer for ssArray"); + return; + } + if (ssArray.size() != MLKEM_SHARED_SECRET_BYTES) { + conscrypt::jniutil::throwIllegalArgumentException(env, "outSharedSecret length != 32"); + return; + } + + CBS cbs; + CBS_init(&cbs, reinterpret_cast(pkArray.get()), pkArray.size()); + MLKEM1024_public_key publicKey; + if (!MLKEM1024_parse_public_key(&publicKey, &cbs) || CBS_len(&cbs) != 0) { + JNI_TRACE("MLKEM1024_parse_public_key failed"); + ERR_clear_error(); + conscrypt::jniutil::throwIllegalArgumentException(env, "MLKEM1024_parse_public_key failed"); + return; + } + + MLKEM1024_encap(reinterpret_cast(ctArray.get()), + reinterpret_cast(ssArray.get()), &publicKey); + JNI_TRACE("MLKEM1024_encap(%p, %p, %p) => success", publicKeyBytes, outCiphertext, + outSharedSecret); +} + +static jbyteArray NativeCrypto_MLKEM1024_decap(JNIEnv* env, jclass, jbyteArray ciphertext, + jbyteArray privateKeySeed) { + CHECK_ERROR_QUEUE_ON_RETURN; + JNI_TRACE("MLKEM1024_decap(%p, %p)", ciphertext, privateKeySeed); + + if (ciphertext == nullptr) { + conscrypt::jniutil::throwNullPointerException(env, "ciphertext == null"); + return nullptr; + } + if (privateKeySeed == nullptr) { + conscrypt::jniutil::throwNullPointerException(env, "privateKeySeed == null"); + return nullptr; + } + + ScopedByteArrayRO ctArray(env, ciphertext); + if (ctArray.get() == nullptr) { + conscrypt::jniutil::throwOutOfMemory(env, "Unable to allocate buffer for ctArray"); + return nullptr; + } + if (ctArray.size() != MLKEM1024_CIPHERTEXT_BYTES) { + conscrypt::jniutil::throwIllegalArgumentException(env, "ciphertext length != 1568"); + return nullptr; + } + + ScopedByteArrayRO seedArray(env, privateKeySeed); + if (seedArray.get() == nullptr) { + conscrypt::jniutil::throwOutOfMemory(env, "Unable to allocate buffer for seedArray"); + return nullptr; + } + if (seedArray.size() != MLKEM_SEED_BYTES) { + conscrypt::jniutil::throwIllegalArgumentException(env, "privateKeySeed length != 64"); + return nullptr; + } + + MLKEM1024_private_key privateKey; + if (!MLKEM1024_private_key_from_seed( + &privateKey, reinterpret_cast(seedArray.get()), seedArray.size())) { + JNI_TRACE("MLKEM1024_private_key_from_seed failed"); + ERR_clear_error(); + conscrypt::jniutil::throwIllegalArgumentException(env, + "MLKEM1024_private_key_from_seed failed"); + return nullptr; + } + + ScopedLocalRef ssRef( + env, env->NewByteArray(static_cast(MLKEM_SHARED_SECRET_BYTES))); + if (ssRef.get() == nullptr) { + conscrypt::jniutil::throwOutOfMemory(env, "Unable to allocate buffer for ssRef"); + return nullptr; + } + ScopedByteArrayRW ssArray(env, ssRef.get()); + if (ssArray.get() == nullptr) { + conscrypt::jniutil::throwOutOfMemory(env, "Unable to allocate buffer for ssArray"); + return nullptr; + } + + if (!MLKEM1024_decap(reinterpret_cast(ssArray.get()), + reinterpret_cast(ctArray.get()), ctArray.size(), + &privateKey)) { + JNI_TRACE("MLKEM1024_decap failed"); + ERR_clear_error(); + conscrypt::jniutil::throwIllegalStateException(env, "MLKEM1024_decap failed"); + return nullptr; + } + + JNI_TRACE("MLKEM1024_decap(%p, %p) => %p", ciphertext, privateKeySeed, ssRef.get()); + return ssRef.release(); +} + static jlong NativeCrypto_EVP_MD_CTX_create(JNIEnv* env, jclass) { CHECK_ERROR_QUEUE_ON_RETURN; JNI_TRACE_MD("EVP_MD_CTX_create()"); @@ -5773,6 +6039,11 @@ static jobject NativeCrypto_EVP_HPKE_CTX_setup_base_mode_recipient( } bssl::UniquePtr ctx(EVP_HPKE_CTX_new()); + if (ctx == nullptr) { + conscrypt::jniutil::throwOutOfMemory(env, "Unable to allocate EVP_HPKE_CTX"); + ERR_clear_error(); + return nullptr; + } ScopedByteArrayRO enc(env, encArray); if (enc.get() == nullptr) { return nullptr; @@ -5848,6 +6119,11 @@ static jobjectArray NativeCrypto_EVP_HPKE_CTX_setup_base_mode_sender(JNIEnv* env uint8_t encapsulatedSharedSecret[EVP_HPKE_MAX_ENC_LENGTH]; bssl::UniquePtr ctx(EVP_HPKE_CTX_new()); + if (ctx == nullptr) { + conscrypt::jniutil::throwOutOfMemory(env, "Unable to allocate EVP_HPKE_CTX"); + ERR_clear_error(); + return {}; + } if (!EVP_HPKE_CTX_setup_sender( /* ctx= */ ctx.get(), @@ -5947,6 +6223,11 @@ static jobjectArray NativeCrypto_EVP_HPKE_CTX_setup_base_mode_sender_with_seed_f uint8_t encapsulatedSharedSecret[EVP_HPKE_MAX_ENC_LENGTH]; bssl::UniquePtr ctx(EVP_HPKE_CTX_new()); + if (ctx == nullptr) { + conscrypt::jniutil::throwOutOfMemory(env, "Unable to allocate EVP_HPKE_CTX"); + ERR_clear_error(); + return nullptr; + } if (!EVP_HPKE_CTX_setup_sender_with_seed_for_testing( /* ctx= */ ctx.get(), @@ -8123,6 +8404,10 @@ static jlongArray X509_CRLs_to_ItemArray(JNIEnv* env, STACK_OF(X509_CRL)* crls) static jbyteArray NativeCrypto_i2d_PKCS7(JNIEnv* env, jclass, jlongArray certsArray) { CHECK_ERROR_QUEUE_ON_RETURN; STACK_OF(X509)* stack = sk_X509_new_null(); + if (stack == nullptr) { + conscrypt::jniutil::throwExceptionFromBoringSSLError(env, "sk_X509_new_null"); + return nullptr; + } ScopedLongArrayRO certs(env, certsArray); if (certs.get() == nullptr) { @@ -8633,6 +8918,11 @@ static jobjectArray NativeCrypto_get_X509_ex_xkusage(JNIEnv* env, jclass, jlong for (size_t i = 0; i < size; i++) { ScopedLocalRef oidStr( env, ASN1_OBJECT_to_OID_string(env, sk_ASN1_OBJECT_value(objArray.get(), i))); + if (oidStr.get() == nullptr) { + // When ASN1_OBJECT_to_OID_string fails, it returns nullptr and adds + // a pending exception. + return nullptr; + } env->SetObjectArrayElement(exKeyUsage.get(), static_cast(i), oidStr.get()); } @@ -9515,7 +9805,13 @@ static jlong NativeCrypto_SSL_new(JNIEnv* env, jclass, jlong ssl_ctx_address, JNI_TRACE("ssl_ctx=%p NativeCrypto_SSL_new appData => 0", ssl_ctx); return 0; } - SSL_set_app_data(ssl.get(), reinterpret_cast(appData)); + if (SSL_set_app_data(ssl.get(), reinterpret_cast(appData)) != 1) { + delete appData; + conscrypt::jniutil::throwSSLExceptionWithSslErrors(env, ssl.get(), SSL_ERROR_NONE, + "Unable to set application data"); + JNI_TRACE("ssl_ctx=%p NativeCrypto_SSL_new SSL_set_app_data => 0", ssl_ctx); + return 0; + } SSL_set_custom_verify(ssl.get(), SSL_VERIFY_PEER, cert_verify_callback); @@ -12487,7 +12783,11 @@ static JNINativeMethod sNativeCryptoMethods[] = { CONSCRYPT_NATIVE_METHOD(ED25519_keypair, "([B[B)V"), CONSCRYPT_NATIVE_METHOD(XWING_public_key_from_seed, "([B)[B"), CONSCRYPT_NATIVE_METHOD(MLKEM768_public_key_from_seed, "([B)[B"), + CONSCRYPT_NATIVE_METHOD(MLKEM768_encap, "([B[B[B)V"), + CONSCRYPT_NATIVE_METHOD(MLKEM768_decap, "([B[B)[B"), CONSCRYPT_NATIVE_METHOD(MLKEM1024_public_key_from_seed, "([B)[B"), + CONSCRYPT_NATIVE_METHOD(MLKEM1024_encap, "([B[B[B)V"), + CONSCRYPT_NATIVE_METHOD(MLKEM1024_decap, "([B[B)[B"), CONSCRYPT_NATIVE_METHOD(EVP_MD_CTX_create, "()J"), CONSCRYPT_NATIVE_METHOD(EVP_MD_CTX_cleanup, "(" REF_EVP_MD_CTX ")V"), CONSCRYPT_NATIVE_METHOD(EVP_MD_CTX_destroy, "(J)V"), diff --git a/common/src/jni/main/include/conscrypt/jniutil.h b/common/src/jni/main/include/conscrypt/jniutil.h index c2b93b106..b8511f2e6 100644 --- a/common/src/jni/main/include/conscrypt/jniutil.h +++ b/common/src/jni/main/include/conscrypt/jniutil.h @@ -207,6 +207,11 @@ extern int throwOutOfMemory(JNIEnv* env, const char* message); */ extern int throwIllegalArgumentException(JNIEnv* env, const char* message); +/** + * Throws an IllegalStateException with the given string as a message. + */ +extern int throwIllegalStateException(JNIEnv* env, const char* message); + /** * Throws a BadPaddingException with the given string as a message. */ diff --git a/common/src/main/java/org/conscrypt/ConscryptEngine.java b/common/src/main/java/org/conscrypt/ConscryptEngine.java index c6a4cc28f..e25873123 100644 --- a/common/src/main/java/org/conscrypt/ConscryptEngine.java +++ b/common/src/main/java/org/conscrypt/ConscryptEngine.java @@ -1692,19 +1692,30 @@ void setApplicationProtocolSelector(ApplicationProtocolSelector selector) { selector == null ? null : new ApplicationProtocolSelectorAdapter(this, selector)); } + // @GuardedBy("stateLock") + private boolean isHandshakeCompletedAndNotClosed() { + return state >= STATE_HANDSHAKE_COMPLETED && state != STATE_CLOSED + && state != STATE_CLOSED_OUTBOUND && !ssl.isClosed(); + } + @Override byte[] getTlsUnique() { - return ssl.getTlsUnique(); + synchronized (stateLock) { + if (!isHandshakeCompletedAndNotClosed()) { + return null; + } + return ssl.getTlsUnique(); + } } @Override byte[] exportKeyingMaterial(String label, byte[] context, int length) throws SSLException { synchronized (stateLock) { - if (state < STATE_HANDSHAKE_COMPLETED || state == STATE_CLOSED) { + if (!isHandshakeCompletedAndNotClosed()) { return null; } + return ssl.exportKeyingMaterial(label, context, length); } - return ssl.exportKeyingMaterial(label, context, length); } void setApplicationProtocolSelector(ApplicationProtocolSelectorAdapter adapter) { diff --git a/common/src/main/java/org/conscrypt/ConscryptX509TrustManager.java b/common/src/main/java/org/conscrypt/ConscryptX509TrustManager.java index 7ee75e0e4..7fe9443e2 100644 --- a/common/src/main/java/org/conscrypt/ConscryptX509TrustManager.java +++ b/common/src/main/java/org/conscrypt/ConscryptX509TrustManager.java @@ -34,9 +34,9 @@ public interface ConscryptX509TrustManager { /** * Verifies the given certificate chain. * - *

See {@link X509TrustManager#checkServerTrusted(X509Certificate[], String)} for a - * description of the chain and authType parameters. The final parameter, host, should be the - * hostname of the server.

+ *

See {@link javax.net.ssl.X509TrustManager#checkServerTrusted(X509Certificate[], String)} + * for a description of the chain and authType parameters. The final parameter, host, should be + * the hostname of the server.

* * @throws CertificateException if the chain does not verify correctly. * @return the properly ordered chain used for verification as a list of X509Certificates. @@ -47,9 +47,9 @@ public List checkServerTrusted(X509Certificate[] chain, String /** * Verifies the given certificate chain. * - *

See {@link X509TrustManager#checkServerTrusted(X509Certificate[], String)} for a - * description of the chain and authType parameters. The final parameter, host, should be the - * hostname of the server. + *

See {@link javax.net.ssl.X509TrustManager#checkServerTrusted(X509Certificate[], String)} + * for a description of the chain and authType parameters. The final parameter, host, should be + * the hostname of the server. * *

ocspData and tlsSctData may be provided to verify any Signed Certificate Timestamp (SCT) * attached to the connection. These are ASN.1 octet strings (SignedCertificateTimestampList) as diff --git a/common/src/main/java/org/conscrypt/Hkdf.java b/common/src/main/java/org/conscrypt/Hkdf.java index bbe13bcbf..c42570081 100644 --- a/common/src/main/java/org/conscrypt/Hkdf.java +++ b/common/src/main/java/org/conscrypt/Hkdf.java @@ -16,11 +16,11 @@ package org.conscrypt; +import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Objects; -import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; /** @@ -28,7 +28,7 @@ *

* Instances should be instantiated using the standard JCA name for the required HMAC. *

- * Each invocation of expand or extract uses a new Mac instance and so instances + * Each invocation of expand or extract uses a new OpenSSLMac.Hmac instance and so instances * of Hkdf are thread-safe.

*/ public final class Hkdf { @@ -38,10 +38,10 @@ public final class Hkdf { /** * Creates an Hkdf instance which will use hmacName as the name for the underlying - * HMAC algorithm, which will be located using normal JCA precedence rules. + * HMAC algorithm, directly using Conscrypt's OpenSSLMac implementation. *

* @param hmacName the name of the HMAC algorithm to use - * @throws NoSuchAlgorithmException if hmacName is not a valid HMAC name + * @throws NoSuchAlgorithmException if hmacName is not a supported HMAC name */ public Hkdf(String hmacName) throws NoSuchAlgorithmException { Objects.requireNonNull(hmacName); @@ -53,7 +53,25 @@ public Hkdf(String hmacName) throws NoSuchAlgorithmException { this.hmacName = hmacName; // Stash the MAC length with the bonus that we'll fail fast here if no such algorithm. - macLength = Mac.getInstance(hmacName).getMacLength(); + OpenSSLMac.Hmac mac = createMac(hmacName); + macLength = mac.engineGetMacLength(); + } + + private static OpenSSLMac.Hmac createMac(String hmacName) throws NoSuchAlgorithmException { + if ("HmacSHA256".equalsIgnoreCase(hmacName)) { + return new OpenSSLMac.HmacSHA256(); + } else if ("HmacSHA384".equalsIgnoreCase(hmacName)) { + return new OpenSSLMac.HmacSHA384(); + } else if ("HmacSHA512".equalsIgnoreCase(hmacName)) { + return new OpenSSLMac.HmacSHA512(); + } else if ("HmacSHA1".equalsIgnoreCase(hmacName)) { + return new OpenSSLMac.HmacSHA1(); + } else if ("HmacSHA224".equalsIgnoreCase(hmacName)) { + return new OpenSSLMac.HmacSHA224(); + } else if ("HmacMD5".equalsIgnoreCase(hmacName)) { + return new OpenSSLMac.HmacMD5(); + } + throw new NoSuchAlgorithmException("Unsupported HMAC algorithm: " + hmacName); } // Visible for testing. @@ -68,9 +86,8 @@ public int getMacLength() { * @param ikm initial keying material * @return a pseudorandom key suitable for use in expand operations * @throws InvalidKeyException if the salt is not suitable for use as an HMAC key - * @throws NoSuchAlgorithmException if the Mac algorithm is no longer available + * @throws NoSuchAlgorithmException if the Mac algorithm is not available */ - public byte[] extract(byte[] salt, byte[] ikm) throws InvalidKeyException, NoSuchAlgorithmException { Objects.requireNonNull(salt); @@ -79,7 +96,9 @@ public byte[] extract(byte[] salt, byte[] ikm) if (salt.length == 0) { salt = new byte[getMacLength()]; } - return getMac(salt).doFinal(ikm); + OpenSSLMac.Hmac mac = getMac(salt); + mac.engineUpdate(ikm, 0, ikm.length); + return mac.engineDoFinal(); } /** @@ -92,7 +111,7 @@ public byte[] extract(byte[] salt, byte[] ikm) * @return output of keying material of length bytes * @throws InvalidKeyException if prk is not suitable for use as an HMAC key * @throws IllegalArgumentException if length is out of the allowed range - * @throws NoSuchAlgorithmException if the Mac algorithm is no longer available + * @throws NoSuchAlgorithmException if the Mac algorithm is not available */ public byte[] expand(byte[] prk, byte[] info, int length) throws InvalidKeyException, NoSuchAlgorithmException { @@ -100,7 +119,7 @@ public byte[] expand(byte[] prk, byte[] info, int length) Objects.requireNonNull(info); Preconditions.checkArgument(length >= 0, "Negative length"); Preconditions.checkArgument(length <= 255 * getMacLength(), "Length too long"); - Mac mac = getMac(prk); + OpenSSLMac.Hmac mac = getMac(prk); int macLength = getMacLength(); byte[] t = new byte[0]; @@ -109,9 +128,10 @@ public byte[] expand(byte[] prk, byte[] info, int length) byte[] counter = new byte[] {0x00}; while (outputOffset < length) { counter[0]++; - mac.update(t); - mac.update(info); - t = mac.doFinal(counter); + mac.engineUpdate(t, 0, t.length); + mac.engineUpdate(info, 0, info.length); + mac.engineUpdate(counter, 0, 1); + t = mac.engineDoFinal(); int size = Math.min(macLength, length - outputOffset); System.arraycopy(t, 0, output, outputOffset, size); outputOffset += size; @@ -119,11 +139,14 @@ public byte[] expand(byte[] prk, byte[] info, int length) return output; } - private Mac getMac(byte[] key) throws InvalidKeyException, NoSuchAlgorithmException { - // Can potentially throw NoSuchAlgorithmException if the there has been a change - // in installed Providers. - Mac mac = Mac.getInstance(hmacName); - mac.init(new SecretKeySpec(key, "RAW")); - return mac; // https://www.youtube.com/watch?v=uB1D9wWxd2w + private OpenSSLMac.Hmac getMac(byte[] key) + throws InvalidKeyException, NoSuchAlgorithmException { + OpenSSLMac.Hmac mac = createMac(hmacName); + try { + mac.engineInit(new SecretKeySpec(key, "RAW"), null); + } catch (InvalidAlgorithmParameterException e) { + throw new AssertionError(e); + } + return mac; } } diff --git a/common/src/main/java/org/conscrypt/NativeCrypto.java b/common/src/main/java/org/conscrypt/NativeCrypto.java index 4b1893383..4fc9d983c 100644 --- a/common/src/main/java/org/conscrypt/NativeCrypto.java +++ b/common/src/main/java/org/conscrypt/NativeCrypto.java @@ -18,10 +18,13 @@ // android-add: import dalvik.annotation.optimization.CriticalNative; // android-add: import dalvik.annotation.optimization.FastNative; + import org.conscrypt.OpenSSLX509CertificateFactory.ParsingException; +import java.io.FileDescriptor; import java.io.IOException; import java.io.OutputStream; +import java.net.SocketTimeoutException; import java.nio.Buffer; import java.nio.ByteBuffer; import java.security.InvalidAlgorithmParameterException; @@ -344,8 +347,23 @@ static native boolean X25519(byte[] out, byte[] privateKey, byte[] publicKey) // --- ML-KEM -------------- static native byte[] MLKEM768_public_key_from_seed(byte[] privateKeySeed); + + // android-add: @FastNative + static native void MLKEM768_encap(byte[] publicKeyBytes, byte[] outCiphertext, + byte[] outSharedSecret); + + // android-add: @FastNative + static native byte[] MLKEM768_decap(byte[] ciphertext, byte[] privateKeySeed); + static native byte[] MLKEM1024_public_key_from_seed(byte[] privateKeySeed); + // android-add: @FastNative + static native void MLKEM1024_encap(byte[] publicKeyBytes, byte[] outCiphertext, + byte[] outSharedSecret); + + // android-add: @FastNative + static native byte[] MLKEM1024_decap(byte[] ciphertext, byte[] privateKeySeed); + // --- Message digest functions -------------- // These return const references diff --git a/common/src/main/java/org/conscrypt/NativeSsl.java b/common/src/main/java/org/conscrypt/NativeSsl.java index 530a94368..a073a39d7 100644 --- a/common/src/main/java/org/conscrypt/NativeSsl.java +++ b/common/src/main/java/org/conscrypt/NativeSsl.java @@ -97,32 +97,74 @@ void offerToResumeSession(long sslSessionNativePointer) throws SSLException { } byte[] getSessionId() { - return NativeCrypto.SSL_session_id(ssl, this); + lock.readLock().lock(); + try { + return isClosed() ? null : NativeCrypto.SSL_session_id(ssl, this); + } finally { + lock.readLock().unlock(); + } } long getTime() { - return NativeCrypto.SSL_get_time(ssl, this); + lock.readLock().lock(); + try { + return isClosed() ? 0L : NativeCrypto.SSL_get_time(ssl, this); + } finally { + lock.readLock().unlock(); + } } long getTimeout() { - return NativeCrypto.SSL_get_timeout(ssl, this); + lock.readLock().lock(); + try { + return isClosed() ? 0L : NativeCrypto.SSL_get_timeout(ssl, this); + } finally { + lock.readLock().unlock(); + } } void setTimeout(long millis) { - NativeCrypto.SSL_set_timeout(ssl, this, millis); + lock.readLock().lock(); + try { + if (!isClosed()) { + NativeCrypto.SSL_set_timeout(ssl, this, millis); + } + } finally { + lock.readLock().unlock(); + } } String getCipherSuite() { - return NativeCrypto.cipherSuiteToJava(NativeCrypto.SSL_get_current_cipher(ssl, this)); + lock.readLock().lock(); + try { + return isClosed() ? null + : NativeCrypto.cipherSuiteToJava( + NativeCrypto.SSL_get_current_cipher(ssl, this)); + } finally { + lock.readLock().unlock(); + } } String getCurveNameForTesting() { - return NativeCrypto.SSL_get_curve_name(ssl, this); + lock.readLock().lock(); + try { + return isClosed() ? null : NativeCrypto.SSL_get_curve_name(ssl, this); + } finally { + lock.readLock().unlock(); + } } X509Certificate[] getPeerCertificates() throws CertificateException { - byte[][] encoded = NativeCrypto.SSL_get0_peer_certificates(ssl, this); - return encoded == null ? null : SSLUtils.decodeX509CertificateChain(encoded); + lock.readLock().lock(); + try { + if (isClosed()) { + return null; + } + byte[][] encoded = NativeCrypto.SSL_get0_peer_certificates(ssl, this); + return encoded == null ? null : SSLUtils.decodeX509CertificateChain(encoded); + } finally { + lock.readLock().unlock(); + } } X509Certificate[] getLocalCertificates() { @@ -130,11 +172,24 @@ X509Certificate[] getLocalCertificates() { } byte[] getPeerCertificateOcspData() { - return NativeCrypto.SSL_get_ocsp_response(ssl, this); + lock.readLock().lock(); + try { + return isClosed() ? null : NativeCrypto.SSL_get_ocsp_response(ssl, this); + } finally { + lock.readLock().unlock(); + } } byte[] getTlsUnique() { - return NativeCrypto.SSL_get_tls_unique(ssl, this); + lock.readLock().lock(); + try { + if (isClosed()) { + return null; + } + return NativeCrypto.SSL_get_tls_unique(ssl, this); + } finally { + lock.readLock().unlock(); + } } byte[] exportKeyingMaterial(String label, byte[] context, int length) throws SSLException { @@ -142,11 +197,24 @@ byte[] exportKeyingMaterial(String label, byte[] context, int length) throws SSL throw new NullPointerException("Label is null"); } byte[] labelBytes = label.getBytes(StandardCharsets.US_ASCII); - return NativeCrypto.SSL_export_keying_material(ssl, this, labelBytes, context, length); + lock.readLock().lock(); + try { + if (isClosed()) { + return null; + } + return NativeCrypto.SSL_export_keying_material(ssl, this, labelBytes, context, length); + } finally { + lock.readLock().unlock(); + } } byte[] getPeerTlsSctData() { - return NativeCrypto.SSL_get_signed_cert_timestamp_list(ssl, this); + lock.readLock().lock(); + try { + return isClosed() ? null : NativeCrypto.SSL_get_signed_cert_timestamp_list(ssl, this); + } finally { + lock.readLock().unlock(); + } } /* @@ -271,11 +339,21 @@ private void setCertificate(String alias) throws CertificateEncodingException, S } String getVersion() { - return NativeCrypto.SSL_get_version(ssl, this); + lock.readLock().lock(); + try { + return isClosed() ? null : NativeCrypto.SSL_get_version(ssl, this); + } finally { + lock.readLock().unlock(); + } } String getRequestedServerName() { - return NativeCrypto.SSL_get_servername(ssl, this); + lock.readLock().lock(); + try { + return isClosed() ? null : NativeCrypto.SSL_get_servername(ssl, this); + } finally { + lock.readLock().unlock(); + } } private static int toBoringSslGroup(String javaNamedGroup) { @@ -336,9 +414,19 @@ static int[] parseNamedGroupsProperty(String namedGroupsProperty) { return toBoringSslGroups(namedGroups); } + private static final int[] DEFAULT_BORINGSSL_NAMED_GROUPS = new int[0]; + + private static final int[] DEFAULT_CLASSICAL_NAMED_GROUPS = + toBoringSslGroups(new String[] {"X25519", "P-256", "P-384"}); + private static void setDefaultNamedGroups(long ssl, NativeSsl sslHolder) { - // When the groups list is empty, the default named groups are used. - NativeCrypto.SSL_set1_groups(ssl, sslHolder, new int[0]); + if (Platform.isPqcDefaultEnabled()) { + // When the groups list is empty, BoringSSL's default named groups + // (DefaultSupportedGroupIds()) are used as the single source of truth. + NativeCrypto.SSL_set1_groups(ssl, sslHolder, DEFAULT_BORINGSSL_NAMED_GROUPS); + } else { + NativeCrypto.SSL_set1_groups(ssl, sslHolder, DEFAULT_CLASSICAL_NAMED_GROUPS); + } } void initialize(String hostname) throws IOException { @@ -555,11 +643,21 @@ TlsEncryptedClientHelloHandshake.Builder getEchHandshakeMetricsBuilder() { } String getEchNameOverride() { - return NativeCrypto.SSL_get0_ech_name_override(ssl, this); + lock.readLock().lock(); + try { + return isClosed() ? null : NativeCrypto.SSL_get0_ech_name_override(ssl, this); + } finally { + lock.readLock().unlock(); + } } byte[] getEchRetryConfigs() { - return NativeCrypto.SSL_get0_ech_retry_configs(ssl, this); + lock.readLock().lock(); + try { + return isClosed() ? null : NativeCrypto.SSL_get0_ech_retry_configs(ssl, this); + } finally { + lock.readLock().unlock(); + } } private void setCertificateValidation() throws SSLException { @@ -692,11 +790,21 @@ boolean isClosed() { } int getError(int result) { - return NativeCrypto.SSL_get_error(ssl, this, result); + lock.readLock().lock(); + try { + return isClosed() ? 0 : NativeCrypto.SSL_get_error(ssl, this, result); + } finally { + lock.readLock().unlock(); + } } byte[] getApplicationProtocol() { - return NativeCrypto.getApplicationProtocol(ssl, this); + lock.readLock().lock(); + try { + return isClosed() ? null : NativeCrypto.getApplicationProtocol(ssl, this); + } finally { + lock.readLock().unlock(); + } } private boolean isClient() { @@ -804,13 +912,13 @@ static synchronized void parseTlsNamedGroupsProperty() { unparsedTlsNamedGroupsProperty = ""; } else { if (!property.equals(unparsedTlsNamedGroupsProperty)) { - unparsedTlsNamedGroupsProperty = property; parsedTlsNamedGroupsProperty = parseNamedGroupsProperty(property); + unparsedTlsNamedGroupsProperty = property; } } } static { - parseTlsNamedGroupsProperty(); + getParsedTlsNamedGroupsPropertyOrNull(); } } diff --git a/common/src/main/java/org/conscrypt/OpenSSLAeadCipher.java b/common/src/main/java/org/conscrypt/OpenSSLAeadCipher.java index a9368fd28..f38726fe2 100644 --- a/common/src/main/java/org/conscrypt/OpenSSLAeadCipher.java +++ b/common/src/main/java/org/conscrypt/OpenSSLAeadCipher.java @@ -86,8 +86,8 @@ public abstract class OpenSSLAeadCipher extends OpenSSLCipher { */ int tagLengthInBytes; - protected OpenSSLAeadCipher(Mode mode) { - super(mode, Padding.NOPADDING); + protected OpenSSLAeadCipher() { + super(Padding.NOPADDING); } private void checkInitialization() { @@ -165,8 +165,7 @@ void engineInitInternal(byte[] encodedKey, AlgorithmParameterSpec params, Secure final int expectedIvLength = NativeCrypto.EVP_AEAD_nonce_length(evpAead); if (iv == null && expectedIvLength != 0) { if (!encrypting) { - throw new InvalidAlgorithmParameterException("IV must be specified in " + mode - + " mode"); + throw new InvalidAlgorithmParameterException("IV must be specified"); } iv = new byte[expectedIvLength]; @@ -176,7 +175,7 @@ void engineInitInternal(byte[] encodedKey, AlgorithmParameterSpec params, Secure NativeCrypto.RAND_bytes(iv); } } else if (expectedIvLength == 0 && iv != null) { - throw new InvalidAlgorithmParameterException("IV not used in " + mode + " mode"); + throw new InvalidAlgorithmParameterException("IV not used"); } else if (iv != null && iv.length != expectedIvLength) { throw new InvalidAlgorithmParameterException("Expected IV length of " + expectedIvLength + " but was " + iv.length); diff --git a/common/src/main/java/org/conscrypt/OpenSSLAeadCipherAES.java b/common/src/main/java/org/conscrypt/OpenSSLAeadCipherAES.java index b7bfb82e9..026701702 100644 --- a/common/src/main/java/org/conscrypt/OpenSSLAeadCipherAES.java +++ b/common/src/main/java/org/conscrypt/OpenSSLAeadCipherAES.java @@ -27,8 +27,8 @@ public abstract class OpenSSLAeadCipherAES extends OpenSSLAeadCipher { private static final int AES_BLOCK_SIZE = 16; - OpenSSLAeadCipherAES(Mode mode) { - super(mode); + OpenSSLAeadCipherAES() { + super(); } @Override @@ -108,12 +108,12 @@ int getOutputSizeForFinal(int inputLen) { public static class GCM extends OpenSSLAeadCipherAES { public GCM() { - super(Mode.GCM); + super(); } @Override - void checkSupportedMode(Mode mode) throws NoSuchAlgorithmException { - if (mode != Mode.GCM) { + protected void engineSetMode(String modeStr) throws NoSuchAlgorithmException { + if (!modeStr.equalsIgnoreCase("GCM")) { throw new NoSuchAlgorithmException("Mode must be GCM"); } } @@ -156,12 +156,12 @@ void checkSupportedKeySize(int keyLength) throws InvalidKeyException { public static class GCM_SIV extends OpenSSLAeadCipherAES { public GCM_SIV() { - super(Mode.GCM_SIV); + super(); } @Override - void checkSupportedMode(Mode mode) throws NoSuchAlgorithmException { - if (mode != Mode.GCM_SIV) { + protected void engineSetMode(String modeStr) throws NoSuchAlgorithmException { + if (!modeStr.equalsIgnoreCase("GCM-SIV")) { throw new NoSuchAlgorithmException("Mode must be GCM-SIV"); } } diff --git a/common/src/main/java/org/conscrypt/OpenSSLAeadCipherChaCha20.java b/common/src/main/java/org/conscrypt/OpenSSLAeadCipherChaCha20.java index da162b9bc..4630d68ab 100644 --- a/common/src/main/java/org/conscrypt/OpenSSLAeadCipherChaCha20.java +++ b/common/src/main/java/org/conscrypt/OpenSSLAeadCipherChaCha20.java @@ -22,7 +22,7 @@ @Internal public class OpenSSLAeadCipherChaCha20 extends OpenSSLAeadCipher { public OpenSSLAeadCipherChaCha20() { - super(Mode.POLY1305); + super(); } @Override @@ -44,8 +44,8 @@ int getCipherBlockSize() { } @Override - void checkSupportedMode(Mode mode) throws NoSuchAlgorithmException { - if (mode != Mode.POLY1305) { + protected void engineSetMode(String modeStr) throws NoSuchAlgorithmException { + if (!modeStr.equalsIgnoreCase("Poly1305")) { throw new NoSuchAlgorithmException("Mode must be Poly1305"); } } diff --git a/common/src/main/java/org/conscrypt/OpenSSLCipher.java b/common/src/main/java/org/conscrypt/OpenSSLCipher.java index 155d757bf..9a472248b 100644 --- a/common/src/main/java/org/conscrypt/OpenSSLCipher.java +++ b/common/src/main/java/org/conscrypt/OpenSSLCipher.java @@ -47,33 +47,6 @@ */ @Internal public abstract class OpenSSLCipher extends CipherSpi { - /** - * Modes that a block cipher may support. - */ - enum Mode { - NONE, - CBC, - CTR, - ECB, - GCM, - GCM_SIV, - POLY1305, - ; - - public static Mode getNormalized(String modeString) { - modeString = modeString.toUpperCase(Locale.US); - // We use GCM-SIV as the mode string, but - isn't a valid identifier character, so - // we need to ensure GCM-SIV is recognized and GCM_SIV isn't. - if (modeString.equals("GCM-SIV")) { - return GCM_SIV; - } else if (modeString.equals("GCM_SIV")) { - throw new IllegalArgumentException("Invalid mode"); - } else { - return Mode.valueOf(modeString); - } - } - } - /** * Paddings that a block cipher may support. */ @@ -92,11 +65,6 @@ public static Padding getNormalized(String value) { } } - /** - * The current cipher mode. - */ - Mode mode = Mode.ECB; - /** * The current cipher padding. */ @@ -125,8 +93,7 @@ public static Padding getNormalized(String value) { OpenSSLCipher() {} - OpenSSLCipher(Mode mode, Padding padding) { - this.mode = mode; + OpenSSLCipher(Padding padding) { this.padding = padding; blockSize = getCipherBlockSize(); } @@ -162,12 +129,6 @@ abstract int updateInternal(byte[] input, int inputOffset, int inputLen, byte[] */ abstract void checkSupportedKeySize(int keySize) throws InvalidKeyException; - /** - * Checks whether the cipher supports this particular cipher {@code mode} - * and throws {@code NoSuchAlgorithmException} if it doesn't. - */ - abstract void checkSupportedMode(Mode mode) throws NoSuchAlgorithmException; - /** * Checks whether the cipher supports this particular cipher {@code padding} * and throws {@code NoSuchPaddingException} if it doesn't. @@ -184,21 +145,6 @@ boolean supportsVariableSizeIv() { return false; } - @Override - protected void engineSetMode(String modeStr) throws NoSuchAlgorithmException { - final Mode mode; - try { - mode = Mode.getNormalized(modeStr); - } catch (IllegalArgumentException e) { - NoSuchAlgorithmException newE = - new NoSuchAlgorithmException("No such mode: " + modeStr); - newE.initCause(e); - throw newE; - } - checkSupportedMode(mode); - this.mode = mode; - } - @Override protected void engineSetPadding(String paddingStr) throws NoSuchPaddingException { final Padding padding; diff --git a/common/src/main/java/org/conscrypt/OpenSSLCipherChaCha20.java b/common/src/main/java/org/conscrypt/OpenSSLCipherChaCha20.java index 6d14d145f..d5f0db0eb 100644 --- a/common/src/main/java/org/conscrypt/OpenSSLCipherChaCha20.java +++ b/common/src/main/java/org/conscrypt/OpenSSLCipherChaCha20.java @@ -177,8 +177,8 @@ void checkSupportedKeySize(int keySize) throws InvalidKeyException { } @Override - void checkSupportedMode(Mode mode) throws NoSuchAlgorithmException { - if (mode != Mode.NONE) { + protected void engineSetMode(String modeStr) throws NoSuchAlgorithmException { + if (!modeStr.equalsIgnoreCase("NONE")) { throw new NoSuchAlgorithmException("Mode must be NONE"); } } diff --git a/common/src/main/java/org/conscrypt/OpenSSLEvpCipher.java b/common/src/main/java/org/conscrypt/OpenSSLEvpCipher.java index c0a6db091..0aac510ee 100644 --- a/common/src/main/java/org/conscrypt/OpenSSLEvpCipher.java +++ b/common/src/main/java/org/conscrypt/OpenSSLEvpCipher.java @@ -48,8 +48,8 @@ public abstract class OpenSSLEvpCipher extends OpenSSLCipher { */ private int modeBlockSize; - protected OpenSSLEvpCipher(Mode mode, Padding padding) { - super(mode, padding); + protected OpenSSLEvpCipher(Padding padding) { + super(padding); } @Override @@ -63,11 +63,10 @@ void engineInitInternal(byte[] encodedKey, AlgorithmParameterSpec params, Secure iv = null; } - final long cipherType = getCipherType(encodedKey.length, mode); + final long cipherType = getCipherType(encodedKey.length); if (cipherType == 0) { throw new InvalidAlgorithmParameterException("Cannot find name for key length = " - + (encodedKey.length * 8) - + " and mode = " + mode); + + (encodedKey.length * 8)); } final boolean encrypting = isEncrypting(); @@ -75,8 +74,7 @@ void engineInitInternal(byte[] encodedKey, AlgorithmParameterSpec params, Secure final int expectedIvLength = NativeCrypto.EVP_CIPHER_iv_length(cipherType); if (iv == null && expectedIvLength != 0) { if (!encrypting) { - throw new InvalidAlgorithmParameterException("IV must be specified in " + mode - + " mode"); + throw new InvalidAlgorithmParameterException("IV must be specified"); } iv = new byte[expectedIvLength]; @@ -86,7 +84,7 @@ void engineInitInternal(byte[] encodedKey, AlgorithmParameterSpec params, Secure NativeCrypto.RAND_bytes(iv); } } else if (expectedIvLength == 0 && iv != null) { - throw new InvalidAlgorithmParameterException("IV not used in " + mode + " mode"); + throw new InvalidAlgorithmParameterException("IV not used"); } else if (iv != null && iv.length != expectedIvLength) { throw new InvalidAlgorithmParameterException("expected IV length of " + expectedIvLength + " but was " + iv.length); @@ -269,10 +267,10 @@ int getOutputSizeForUpdate(int inputLen) { } /** - * Returns the OpenSSL cipher for the particular {@code keySize} - * and cipher {@code mode}, or 0 if the combination is not supported. + * Returns the OpenSSL cipher for the particular {@code keySize}, + * or 0 if it is not supported. */ - abstract long getCipherType(int keySize, Mode mode); + abstract long getCipherType(int keySize); /** * Reset this Cipher instance state to process a new chunk of data. diff --git a/common/src/main/java/org/conscrypt/OpenSSLEvpCipherAES.java b/common/src/main/java/org/conscrypt/OpenSSLEvpCipherAES.java index 1c0a45abd..581dee59e 100644 --- a/common/src/main/java/org/conscrypt/OpenSSLEvpCipherAES.java +++ b/common/src/main/java/org/conscrypt/OpenSSLEvpCipherAES.java @@ -18,6 +18,7 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.Locale; import javax.crypto.NoSuchPaddingException; @@ -25,19 +26,28 @@ public abstract class OpenSSLEvpCipherAES extends OpenSSLEvpCipher { private static final int AES_BLOCK_SIZE = 16; + enum Mode { + CBC, + CTR, + ECB, + } + + private Mode mode; + OpenSSLEvpCipherAES(Mode mode, Padding padding) { - super(mode, padding); + super(padding); + this.mode = mode; } @Override - void checkSupportedMode(Mode mode) throws NoSuchAlgorithmException { - switch (mode) { - case CBC: - case CTR: - case ECB: - return; - default: - throw new NoSuchAlgorithmException("Unsupported mode " + mode.toString()); + protected void engineSetMode(String modeStr) throws NoSuchAlgorithmException { + try { + this.mode = Mode.valueOf(modeStr.toUpperCase(Locale.US)); + } catch (IllegalArgumentException e) { + NoSuchAlgorithmException newE = + new NoSuchAlgorithmException("No such mode: " + modeStr); + newE.initCause(e); + throw newE; } } @@ -58,7 +68,7 @@ String getBaseCipherName() { } @Override - long getCipherType(int keyLength, Mode mode) { + long getCipherType(int keyLength) { switch (mode) { case CBC: switch (keyLength) { diff --git a/common/src/main/java/org/conscrypt/OpenSSLEvpCipherARC4.java b/common/src/main/java/org/conscrypt/OpenSSLEvpCipherARC4.java index 4adb66216..f281e4305 100644 --- a/common/src/main/java/org/conscrypt/OpenSSLEvpCipherARC4.java +++ b/common/src/main/java/org/conscrypt/OpenSSLEvpCipherARC4.java @@ -25,7 +25,7 @@ public class OpenSSLEvpCipherARC4 extends OpenSSLEvpCipher { public OpenSSLEvpCipherARC4() { // Modes and padding don't make sense for ARC4. - super(Mode.ECB, Padding.NOPADDING); + super(Padding.NOPADDING); } @Override @@ -34,7 +34,7 @@ String getBaseCipherName() { } @Override - long getCipherType(int keySize, Mode mode) { + long getCipherType(int keySize) { return NativeCrypto.EVP_rc4(); } @@ -42,9 +42,9 @@ long getCipherType(int keySize, Mode mode) { void checkSupportedKeySize(int keySize) throws InvalidKeyException {} @Override - void checkSupportedMode(Mode mode) throws NoSuchAlgorithmException { - if (mode != Mode.NONE && mode != Mode.ECB) { - throw new NoSuchAlgorithmException("Unsupported mode " + mode.toString()); + protected void engineSetMode(String modeStr) throws NoSuchAlgorithmException { + if (!modeStr.equalsIgnoreCase("NONE") && !modeStr.equalsIgnoreCase("ECB")) { + throw new NoSuchAlgorithmException("Unsupported mode " + modeStr); } } diff --git a/common/src/main/java/org/conscrypt/OpenSSLEvpCipherDESEDE.java b/common/src/main/java/org/conscrypt/OpenSSLEvpCipherDESEDE.java index 95bd77f39..88c26fdce 100644 --- a/common/src/main/java/org/conscrypt/OpenSSLEvpCipherDESEDE.java +++ b/common/src/main/java/org/conscrypt/OpenSSLEvpCipherDESEDE.java @@ -25,13 +25,13 @@ public abstract class OpenSSLEvpCipherDESEDE extends OpenSSLEvpCipher { private static final int DES_BLOCK_SIZE = 8; - OpenSSLEvpCipherDESEDE(Mode mode, Padding padding) { - super(mode, padding); + OpenSSLEvpCipherDESEDE(Padding padding) { + super(padding); } public static class CBC extends OpenSSLEvpCipherDESEDE { CBC(Padding padding) { - super(Mode.CBC, padding); + super(padding); } public static class NoPadding extends CBC { @@ -53,13 +53,11 @@ String getBaseCipherName() { } @Override - long getCipherType(int keySize, Mode mode) { - if (mode == Mode.CBC) { - if (keySize == 16) { - return NativeCrypto.EVP_des_ede_cbc(); - } else if (keySize == 24) { - return NativeCrypto.EVP_des_ede3_cbc(); - } + long getCipherType(int keySize) { + if (keySize == 16) { + return NativeCrypto.EVP_des_ede_cbc(); + } else if (keySize == 24) { + return NativeCrypto.EVP_des_ede3_cbc(); } return 0; } @@ -72,9 +70,9 @@ void checkSupportedKeySize(int keySize) throws InvalidKeyException { } @Override - void checkSupportedMode(Mode mode) throws NoSuchAlgorithmException { - if (mode != Mode.CBC) { - throw new NoSuchAlgorithmException("Unsupported mode " + mode.toString()); + protected void engineSetMode(String modeStr) throws NoSuchAlgorithmException { + if (!modeStr.equalsIgnoreCase("CBC")) { + throw new NoSuchAlgorithmException("Unsupported mode " + modeStr); } } diff --git a/common/src/main/java/org/conscrypt/OpenSSLProvider.java b/common/src/main/java/org/conscrypt/OpenSSLProvider.java index d55636a92..295434d40 100644 --- a/common/src/main/java/org/conscrypt/OpenSSLProvider.java +++ b/common/src/main/java/org/conscrypt/OpenSSLProvider.java @@ -791,6 +791,23 @@ public OpenSSLProvider(String providerName) { put("TrustManagerFactory.PAKE", PREFIX + "PakeTrustManagerFactory"); put("KeyManagerFactory.PAKE", PREFIX + "PakeKeyManagerFactory"); } + + /* === KDF === */ + if (classExists("javax.crypto.KDF") && Platform.isPqcKemKdfSupported()) { + // Standard JCA KDF algorithm names per Java Security Standard Algorithm Names spec. + put("KDF.HKDF-SHA256", PREFIX + "OpenSslHkdf$HkdfSha256"); + put("KDF.HKDF-SHA384", PREFIX + "OpenSslHkdf$HkdfSha384"); + put("KDF.HKDF-SHA512", PREFIX + "OpenSslHkdf$HkdfSha512"); + put("KDF.HKDF-SHA1", PREFIX + "OpenSslHkdf$HkdfSha1"); + } + + /* === KEM === */ + if (classExists("javax.crypto.KEM") && Platform.isPqcKemKdfSupported()) { + // Standard JCA KEM algorithm names per Java Security Standard Algorithm Names spec. + put("KEM.ML-KEM", PREFIX + "OpenSslMlKem$MlKem"); + put("KEM.ML-KEM-768", PREFIX + "OpenSslMlKem$MlKem768"); + put("KEM.ML-KEM-1024", PREFIX + "OpenSslMlKem$MlKem1024"); + } } @SuppressWarnings("UnusedMethod") diff --git a/common/src/main/java/org/conscrypt/ct/CertificateEntry.java b/common/src/main/java/org/conscrypt/ct/CertificateEntry.java index d28b03cd2..95201acae 100644 --- a/common/src/main/java/org/conscrypt/ct/CertificateEntry.java +++ b/common/src/main/java/org/conscrypt/ct/CertificateEntry.java @@ -18,6 +18,7 @@ import org.conscrypt.Internal; import org.conscrypt.OpenSSLX509Certificate; +import org.conscrypt.util.SerializationException; import java.io.OutputStream; import java.security.MessageDigest; diff --git a/common/src/main/java/org/conscrypt/ct/DigitallySigned.java b/common/src/main/java/org/conscrypt/ct/DigitallySigned.java index c8bef16bd..d30b9e6c6 100644 --- a/common/src/main/java/org/conscrypt/ct/DigitallySigned.java +++ b/common/src/main/java/org/conscrypt/ct/DigitallySigned.java @@ -17,6 +17,7 @@ package org.conscrypt.ct; import org.conscrypt.Internal; +import org.conscrypt.util.SerializationException; import java.io.ByteArrayInputStream; import java.io.InputStream; diff --git a/common/src/main/java/org/conscrypt/ct/LogInfo.java b/common/src/main/java/org/conscrypt/ct/LogInfo.java index 6412f2fe7..405dfc10c 100644 --- a/common/src/main/java/org/conscrypt/ct/LogInfo.java +++ b/common/src/main/java/org/conscrypt/ct/LogInfo.java @@ -17,6 +17,7 @@ package org.conscrypt.ct; import org.conscrypt.Internal; +import org.conscrypt.util.SerializationException; import java.security.InvalidKeyException; import java.security.MessageDigest; diff --git a/common/src/main/java/org/conscrypt/ct/Serialization.java b/common/src/main/java/org/conscrypt/ct/Serialization.java index ff570bfda..c7adb5b47 100644 --- a/common/src/main/java/org/conscrypt/ct/Serialization.java +++ b/common/src/main/java/org/conscrypt/ct/Serialization.java @@ -17,6 +17,7 @@ package org.conscrypt.ct; import org.conscrypt.Internal; +import org.conscrypt.util.SerializationException; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -28,30 +29,6 @@ public class Serialization { private Serialization() {} - private static final int DER_TAG_MASK = 0x3f; - private static final int DER_TAG_OCTET_STRING = 0x4; - private static final int DER_LENGTH_LONG_FORM_FLAG = 0x80; - - public static byte[] readDEROctetString(byte[] input) throws SerializationException { - return readDEROctetString(new ByteArrayInputStream(input)); - } - - public static byte[] readDEROctetString(InputStream input) throws SerializationException { - int tag = readByte(input) & DER_TAG_MASK; - if (tag != DER_TAG_OCTET_STRING) { - throw new SerializationException("Wrong DER tag, expected OCTET STRING, got " + tag); - } - int length; - int width = readNumber(input, 1); - if ((width & DER_LENGTH_LONG_FORM_FLAG) != 0) { - length = readNumber(input, width & ~DER_LENGTH_LONG_FORM_FLAG); - } else { - length = width; - } - - return readFixedBytes(input, length); - } - public static byte[][] readList(byte[] input, int listWidth, int elemWidth) throws SerializationException { return readList(new ByteArrayInputStream(input), listWidth, elemWidth); diff --git a/common/src/main/java/org/conscrypt/ct/SignedCertificateTimestamp.java b/common/src/main/java/org/conscrypt/ct/SignedCertificateTimestamp.java index 9e89f5ce9..3cc0c3596 100644 --- a/common/src/main/java/org/conscrypt/ct/SignedCertificateTimestamp.java +++ b/common/src/main/java/org/conscrypt/ct/SignedCertificateTimestamp.java @@ -17,6 +17,7 @@ package org.conscrypt.ct; import org.conscrypt.Internal; +import org.conscrypt.util.SerializationException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; diff --git a/common/src/main/java/org/conscrypt/ct/Verifier.java b/common/src/main/java/org/conscrypt/ct/Verifier.java index db1e2e89f..35a8afb88 100644 --- a/common/src/main/java/org/conscrypt/ct/Verifier.java +++ b/common/src/main/java/org/conscrypt/ct/Verifier.java @@ -19,6 +19,8 @@ import org.conscrypt.Internal; import org.conscrypt.NativeCrypto; import org.conscrypt.OpenSSLX509Certificate; +import org.conscrypt.util.DerUtils; +import org.conscrypt.util.SerializationException; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; @@ -231,9 +233,8 @@ private List getSCTsFromOCSPResponse( } try { - return getSCTsFromSCTList( - Serialization.readDEROctetString(Serialization.readDEROctetString(extData)), - SignedCertificateTimestamp.Origin.OCSP_RESPONSE); + return getSCTsFromSCTList(DerUtils.readOctetString(DerUtils.readOctetString(extData)), + SignedCertificateTimestamp.Origin.OCSP_RESPONSE); } catch (SerializationException e) { return Collections.emptyList(); } @@ -252,9 +253,8 @@ private List getSCTsFromX509Extension(OpenSSLX509Cer } try { - return getSCTsFromSCTList( - Serialization.readDEROctetString(Serialization.readDEROctetString(extData)), - SignedCertificateTimestamp.Origin.EMBEDDED); + return getSCTsFromSCTList(DerUtils.readOctetString(DerUtils.readOctetString(extData)), + SignedCertificateTimestamp.Origin.EMBEDDED); } catch (SerializationException e) { return Collections.emptyList(); } diff --git a/common/src/main/java/org/conscrypt/metrics/ConscryptStatsLog.java b/common/src/main/java/org/conscrypt/metrics/ConscryptStatsLog.java index c035accab..b6f689cce 100644 --- a/common/src/main/java/org/conscrypt/metrics/ConscryptStatsLog.java +++ b/common/src/main/java/org/conscrypt/metrics/ConscryptStatsLog.java @@ -254,6 +254,7 @@ public final class ConscryptStatsLog { public static final int TLS_ENCRYPTED_CLIENT_HELLO_HANDSHAKE_REPORTED__FAILURE_REASON__ECH_FAILURE_REASON_INVALID_CONFIG = 2; public static final int TLS_ENCRYPTED_CLIENT_HELLO_HANDSHAKE_REPORTED__FAILURE_REASON__ECH_FAILURE_REASON_INCONSISTENT_NEGOTIATION = 3; public static final int TLS_ENCRYPTED_CLIENT_HELLO_HANDSHAKE_REPORTED__FAILURE_REASON__ECH_FAILURE_REASON_NO_RETRY_CONFIGS = 4; + public static final int TLS_ENCRYPTED_CLIENT_HELLO_HANDSHAKE_REPORTED__FAILURE_REASON__ECH_FAILURE_REASON_FAILED_TLS_HANDSHAKE = 5; // Write methods public static void write(int code, boolean arg1, int arg2, int arg3, int arg4, int arg5, int[] arg6) { diff --git a/common/src/main/java/org/conscrypt/metrics/TlsEncryptedClientHelloHandshake.java b/common/src/main/java/org/conscrypt/metrics/TlsEncryptedClientHelloHandshake.java index e41eb97a1..b734b7903 100644 --- a/common/src/main/java/org/conscrypt/metrics/TlsEncryptedClientHelloHandshake.java +++ b/common/src/main/java/org/conscrypt/metrics/TlsEncryptedClientHelloHandshake.java @@ -109,7 +109,9 @@ public enum FailureReason { INCONSISTENT_NEGOTIATION( ConscryptStatsLog.TLS_ENCRYPTED_CLIENT_HELLO_HANDSHAKE_REPORTED__FAILURE_REASON__ECH_FAILURE_REASON_INCONSISTENT_NEGOTIATION), NO_RETRY_CONFIG( - ConscryptStatsLog.TLS_ENCRYPTED_CLIENT_HELLO_HANDSHAKE_REPORTED__FAILURE_REASON__ECH_FAILURE_REASON_NO_RETRY_CONFIGS); + ConscryptStatsLog.TLS_ENCRYPTED_CLIENT_HELLO_HANDSHAKE_REPORTED__FAILURE_REASON__ECH_FAILURE_REASON_NO_RETRY_CONFIGS), + FAILED_TLS_HANDSHAKE( + ConscryptStatsLog.TLS_ENCRYPTED_CLIENT_HELLO_HANDSHAKE_REPORTED__FAILURE_REASON__ECH_FAILURE_REASON_FAILED_TLS_HANDSHAKE); private final int metricsValue; @@ -244,10 +246,18 @@ private SkipReason calculateSkipReason() { return SkipReason.UNKNOWN; } + private FailureReason calculateFailureReason() { + if (!handshakeSuccess && failureReason == FailureReason.UNKNOWN) { + return FailureReason.FAILED_TLS_HANDSHAKE; + } + + return failureReason; + } + public TlsEncryptedClientHelloHandshake build() { return new TlsEncryptedClientHelloHandshake( calculateResult(), calculateUsageReason(), calculateSkipReason(), - failureReason, handshakeDurationMillis); + calculateFailureReason(), handshakeDurationMillis); } } diff --git a/common/src/main/java/org/conscrypt/util/DerUtils.java b/common/src/main/java/org/conscrypt/util/DerUtils.java new file mode 100644 index 000000000..58307ffd8 --- /dev/null +++ b/common/src/main/java/org/conscrypt/util/DerUtils.java @@ -0,0 +1,353 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * 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 org.conscrypt.util; + +import org.conscrypt.Internal; + +import java.math.BigInteger; +import java.nio.ByteBuffer; + +/** + * Helpers for reading DER-encoded data (the binary format used by X.509 certificates). + * + *

Every DER item is laid out as {@code [Tag (1 byte)][Length (1-5 bytes)][Content]}: + *

    + *
  • {@code readOctetString} / {@code readRelativeOidBytes} strip the header and return only + * the inner {@code [Content]} bytes. + *
  • {@code readSequenceElement} / {@code readOptionalElement} / {@code readElement} + * return the entire {@code [Tag][Length][Content]} slice intact (for copying or hashing raw + * fields). + *
  • {@code readSequence} / {@code readContent} step into a struct by advancing the + * outer buffer past the entire element and returning a zero-copy {@link ByteBuffer} slice + * bounded to the inner {@code [Content]} bytes. + *
  • {@code skipSequence} / {@code skipInteger} jump completely over an entire + * {@code [Tag][Length][Content]} item to reach the next field. + *
+ * + *

This class only supports single-byte tags (tag numbers < 31, rejecting multi-byte tag + * marker {@code 0x1F}) and lengths up to 4 bytes, which covers all X.509, CT, and MTC structures. + */ +@Internal +public final class DerUtils { + // Tag bytes identifying the data type of the [Content] that follows: + private static final int TAG_INTEGER = 0x02; // Whole number (e.g. certificate serial number) + private static final int TAG_OCTET_STRING = 0x04; // Raw byte[] blob + private static final int TAG_OID = + 0x06; // ASN.1 OBJECT IDENTIFIER (e.g. "1.3.6.1.4.1.44363.47.3") + private static final int TAG_RELATIVE_OID = 0x0D; // Base-128 relative OID (e.g. "32473.1") + private static final int TAG_SEQUENCE = 0x30; // Struct / ordered group of nested DER fields + private static final int TAG_SET = 0x31; // ASN.1 SET (e.g. RelativeDistinguishedName) + + // Context-specific constructed tag [0] EXPLICIT (0xA0), e.g. for optional X.509 TBSCertificate + // version field. + public static final int TAG_CONTEXT_EXPLICIT_0 = 0xA0; + + private DerUtils() {} + + /** + * Reads a struct ({@code SEQUENCE}) and returns the full {@code [Tag][Length][Content]} bytes + * intact, preserving the outer header so the field can be copied or hashed as-is. + */ + public static byte[] readSequenceElement(ByteBuffer buf) throws SerializationException { + return readElement(buf, TAG_SEQUENCE); + } + + /** + * Jumps completely over an entire struct ({@code [Tag][Length][Content]}) to land on the next + * sibling field without allocating a byte array. + */ + public static void skipSequence(ByteBuffer buf) throws SerializationException { + skipElement(buf, TAG_SEQUENCE); + } + + /** + * Steps inside a struct ({@code SEQUENCE}) by verifying its tag, advancing {@code buf} past + * the entire sequence, and returning a zero-copy {@link ByteBuffer} slice bounded to the + * inner {@code [Content]} bytes. + */ + public static ByteBuffer readSequence(ByteBuffer buf) throws SerializationException { + return readContent(buf, TAG_SEQUENCE); + } + + /** + * Steps inside an ASN.1 {@code SET} (tag {@code 0x31}) by verifying its tag, advancing + * {@code buf} past the entire set, and returning a zero-copy {@link ByteBuffer} slice + * bounded to the inner {@code [Content]} bytes. + */ + public static ByteBuffer readSet(ByteBuffer buf) throws SerializationException { + return readContent(buf, TAG_SET); + } + + /** + * Jumps completely over an entire integer field ({@code [Tag][Length][Content]}) to land on + * the next sibling field without allocating a byte array. + */ + public static void skipInteger(ByteBuffer buf) throws SerializationException { + skipElement(buf, TAG_INTEGER); + } + + /** + * Checks if an optional element with {@code expectedTag} is present at the current buffer + * position. If so, reads and returns its full {@code [Tag][Length][Content]} bytes; if + * absent, leaves the buffer untouched and returns {@code null}. + */ + public static byte[] readOptionalElement(ByteBuffer buf, int expectedTag) + throws SerializationException { + if (!buf.hasRemaining()) { + return null; + } + if (Byte.toUnsignedInt(buf.get(buf.position())) == expectedTag) { + return readElement(buf, expectedTag); + } + return null; + } + + /** + * Verifies that the next item has {@code expectedTag} and returns its complete + * {@code [Tag][Length][Content]} bytes. + */ + public static byte[] readElement(ByteBuffer buf, int expectedTag) + throws SerializationException { + int tagPosition = buf.position(); + int valueLen; + try { + int tag = readTag(buf); + if (tag != expectedTag) { + throw new SerializationException("Wrong DER tag, expected " + expectedTag + ", got " + + tag); + } + valueLen = readLength(buf); + } catch (SerializationException e) { + buf.position(tagPosition); + throw e; + } + int headerLen = buf.position() - tagPosition; + + byte[] tlv = new byte[headerLen + valueLen]; + // Rewind to tagPosition so buf.get(tlv) copies the entire [Tag][Length][Content] slice. + buf.position(tagPosition); + buf.get(tlv); + return tlv; + } + + /** + * Verifies that the next item has {@code expectedTag} and advances the buffer past the entire + * {@code [Tag][Length][Content]} item. + */ + public static void skipElement(ByteBuffer buf, int expectedTag) throws SerializationException { + readContent(buf, expectedTag); + } + + /** + * Verifies that the next item has {@code expectedTag}, advances {@code buf} past the entire + * {@code [Tag][Length][Content]} item, and returns a zero-copy {@link ByteBuffer} slice + * bounded to its inner {@code [Content]} bytes. + */ + public static ByteBuffer readContent(ByteBuffer buf, int expectedTag) + throws SerializationException { + int tagPosition = buf.position(); + int valueLen; + try { + int tag = readTag(buf); + if (tag != expectedTag) { + throw new SerializationException("Wrong DER tag, expected " + expectedTag + ", got " + + tag); + } + valueLen = readLength(buf); + } catch (SerializationException e) { + buf.position(tagPosition); + throw e; + } + ByteBuffer content = buf.slice(); + content.limit(valueLen); + buf.position(buf.position() + valueLen); + return content; + } + + /** + * Reads a DER INTEGER (tag {@code 0x02}) and returns its value as a {@link BigInteger}. + */ + public static BigInteger readBigInteger(ByteBuffer buf) throws SerializationException { + ByteBuffer content = readContent(buf, TAG_INTEGER); + if (!content.hasRemaining()) { + throw new SerializationException("Empty DER INTEGER"); + } + byte[] bytes = remainingBytesToArray(content); + if (bytes.length > 1) { + if ((bytes[0] == 0x00 && (bytes[1] & 0x80) == 0) + || (bytes[0] == (byte) 0xFF && (bytes[1] & 0x80) != 0)) { + throw new SerializationException("Non-minimal DER INTEGER encoding"); + } + } + return new BigInteger(bytes); + } + + /** + * Reads a DER OBJECT IDENTIFIER (tag {@code 0x06}) and decodes it into a dot-separated + * decimal string (e.g. {@code "1.3.6.1.4.1.44363.47.3"}). + */ + public static String readOid(ByteBuffer buf) throws SerializationException { + ByteBuffer content = readContent(buf, TAG_OID); + long firstSubId = readBase128SubIdentifier(content); + long firstArc = Math.min(2L, firstSubId / 40L); + long secondArc = firstSubId - (firstArc * 40L); + StringBuilder sb = new StringBuilder(); + sb.append(firstArc).append('.').append(secondArc); + while (content.hasRemaining()) { + sb.append('.').append(readBase128SubIdentifier(content)); + } + return sb.toString(); + } + + /** + * Reads a DER RELATIVE-OID (tag {@code 0x0D}) from {@code buf}, validates that its base-128 + * subidentifiers are well-formed and minimally encoded, and returns its raw content bytes. + */ + public static byte[] readRelativeOidBytes(ByteBuffer buf) throws SerializationException { + ByteBuffer content = readContent(buf, TAG_RELATIVE_OID); + do { + readBase128SubIdentifier(content); + } while (content.hasRemaining()); + // readContent returns a ByteBuffer.slice(), so rewind() resets position to the start of + // the RELATIVE-OID content (not the start of the outer backing buffer). + content.rewind(); + return remainingBytesToArray(content); + } + + /** + * Decodes raw base-128 RELATIVE-OID content bytes (without tag/length header) into a + * canonical dot-separated decimal string (e.g. {@code "32473.1"}). + */ + public static String formatRelativeOid(byte[] relativeOidBytes) throws SerializationException { + if (relativeOidBytes == null || relativeOidBytes.length == 0) { + throw new SerializationException("Empty RELATIVE-OID bytes"); + } + ByteBuffer content = ByteBuffer.wrap(relativeOidBytes); + StringBuilder sb = new StringBuilder(); + while (content.hasRemaining()) { + if (sb.length() > 0) { + sb.append('.'); + } + sb.append(readBase128SubIdentifier(content)); + } + return sb.toString(); + } + + private static long readBase128SubIdentifier(ByteBuffer content) throws SerializationException { + long value = 0; + boolean firstByte = true; + while (content.hasRemaining()) { + int b = Byte.toUnsignedInt(content.get()); + if (firstByte && b == 0x80) { + throw new SerializationException("Non-minimal base-128 OID component"); + } + firstByte = false; + if ((value & (0x7FL << 56)) != 0) { + throw new SerializationException("OID component exceeds 63 bits"); + } + value = (value << 7) | (b & 0x7F); + if ((b & 0x80) == 0) { + return value; + } + } + throw new SerializationException("Truncated base-128 OID component"); + } + + /** + * Unwraps a raw byte array ({@code OCTET STRING}, {@code 0x04}) and returns only its inner + * {@code [Content]} bytes (without the tag and length header). + */ + public static byte[] readOctetString(ByteBuffer buf) throws SerializationException { + return remainingBytesToArray(readContent(buf, TAG_OCTET_STRING)); + } + + private static byte[] remainingBytesToArray(ByteBuffer buf) { + byte[] bytes = new byte[buf.remaining()]; + buf.get(bytes); + return bytes; + } + + /** + * Reads a DER-encoded OCTET STRING from a byte array, verifying that no trailing bytes follow, + * and returns its payload bytes. + */ + public static byte[] readOctetString(byte[] input) throws SerializationException { + if (input == null) { + throw new SerializationException("Input is null"); + } + ByteBuffer buf = ByteBuffer.wrap(input); + byte[] payload = readOctetString(buf); + if (buf.hasRemaining()) { + throw new SerializationException("Trailing data after DER OCTET STRING"); + } + return payload; + } + + private static int readTag(ByteBuffer buf) throws SerializationException { + if (!buf.hasRemaining()) { + throw new SerializationException("Unexpected end of DER encoding"); + } + int tag = Byte.toUnsignedInt(buf.get()); + // Bits 5-1 set to all ones (0x1F) indicates high tag number form (multi-byte tag). + // Standard X.509 certificates only use single-byte tags (< 31). + if ((tag & 0x1F) == 0x1F) { + throw new SerializationException("High tag number form not supported"); + } + return tag; + } + + private static int readLength(ByteBuffer buf) throws SerializationException { + if (!buf.hasRemaining()) { + throw new SerializationException("Unexpected end of DER encoding"); + } + int initialByte = Byte.toUnsignedInt(buf.get()); + return (initialByte < 128) ? readShortFormLength(buf, initialByte) + : readLongFormLength(buf, initialByte); + } + + private static int readShortFormLength(ByteBuffer buf, int initialByte) + throws SerializationException { + int len = initialByte; + if (buf.remaining() < len) { + throw new SerializationException("DER element length exceeds data bounds"); + } + return len; + } + + private static int readLongFormLength(ByteBuffer buf, int initialByte) + throws SerializationException { + int numBytes = initialByte & 0x7F; + if (numBytes == 0 || numBytes > 4 || buf.remaining() < numBytes) { + throw new SerializationException("Invalid DER length encoding"); + } + int len = 0; + for (int i = 0; i < numBytes; i++) { + len = (len << 8) | Byte.toUnsignedInt(buf.get()); + } + // DER requires minimal length encoding (ITU-T X.690 Section 10.1): + // 1. Long form cannot be used for lengths < 128 (must use short form). + // 2. Multi-byte lengths (numBytes > 1) cannot have leading zero bytes; + // the value must be >= 1 << ((numBytes - 1) * 8), meaning it cannot fit in fewer bytes. + if (len < 128 || len < (1 << ((numBytes - 1) * 8))) { + throw new SerializationException("Non-minimal DER length encoding"); + } + if (len < 0 || buf.remaining() < len) { + throw new SerializationException("DER element length exceeds data bounds"); + } + return len; + } +} diff --git a/common/src/main/java/org/conscrypt/util/MtcUtils.java b/common/src/main/java/org/conscrypt/util/MtcUtils.java new file mode 100644 index 000000000..6883d2803 --- /dev/null +++ b/common/src/main/java/org/conscrypt/util/MtcUtils.java @@ -0,0 +1,526 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * 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 org.conscrypt.util; + +import org.conscrypt.ConscryptCertStore; +import org.conscrypt.Internal; + +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Set; + +import javax.security.auth.x500.X500Principal; + +/** + * Utility methods for draft-ietf-plants-merkle-tree-certs-06 (MTC) ID parsing and message + * construction. + */ +@Internal +public final class MtcUtils { + private static final String MTC_PROOF_SIG_ALG_OID = "1.3.6.1.4.1.44363.47.0"; + private static final String MTC_CA_ID_OID = "1.3.6.1.4.1.44363.47.3"; + private static final String MTC_CA_EXT_SHA256_OID = "1.3.6.1.4.1.44363.47.4"; + private static final String ML_DSA_44_OID = "2.16.840.1.101.3.4.3.17"; + private static final String ML_DSA_65_OID = "2.16.840.1.101.3.4.3.18"; + private static final String ML_DSA_87_OID = "2.16.840.1.101.3.4.3.19"; + public static final int MTC_INDEX_BITS = 48; + public static final long MTC_INDEX_MASK = (1L << MTC_INDEX_BITS) - 1; + private static final BigInteger MIN_MTC_SERIAL = BigInteger.ONE.shiftLeft(MTC_INDEX_BITS); + private static final BigInteger MAX_MTC_SERIAL = + BigInteger.ONE.shiftLeft(64).subtract(BigInteger.ONE); + public static final byte MTC_LEAF_PREFIX = 0x00; + public static final short ENTRY_TYPE_TBS_CERT = 0x0001; + + private static final String MTC_TAI_PREFIX = "oid/1.3.6.1.4.1."; + private static final byte[] MTC_SUBTREE_V1_LABEL = + new byte[] {'s', 'u', 'b', 't', 'r', 'e', 'e', '/', 'v', '1', '\n', 0}; + + private MtcUtils() {} + + /** + * Checks whether the given certificate uses the MTC proof signature algorithm per + * {@code draft-ietf-plants-merkle-tree-certs-06 Section 6.2}. + * + *

An MTC signature is identified by having a synthesized signature algorithm indicating + * an MTC proof (OID {@code 1.3.6.1.4.1.44363.47.0}, {@code id-alg-mtcProof}) with omitted + * parameters. + * + *

Note: Per Section 7.2, dispatching between classic X.509 signature verification and + * MTC verification must be determined by whether the issuer is an MTC CA + * ({@link #isMtcCaCertificate}), not solely by the subject certificate's signature algorithm. + */ + public static boolean hasMtcSignatureAlgorithm(X509Certificate cert) { + if (cert == null) { + return false; + } + return MTC_PROOF_SIG_ALG_OID.equals(cert.getSigAlgOID()) && cert.getSigAlgParams() == null; + } + + /** + * Checks whether the given certificate is an MTC CA / Trust Anchor certificate per + * {@code draft-ietf-plants-merkle-tree-certs-06 Section 5.5}. + * + *

An MTC CA certificate is identified by the presence of the MTC CA extension + * (OID {@code 1.3.6.1.4.1.44363.47.4}, {@code id-pe-mtcCertificationAuthority-SHA256}). + * Structural validity of the extension (such as criticality and serial bounds) and the + * Subject DN (Trust Anchor ID) is enforced separately during verification so that a + * malformed MTC CA certificate never falls back to classic X.509 verification. + */ + public static boolean isMtcCaCertificate(X509Certificate cert) { + if (cert == null) { + return false; + } + return cert.getExtensionValue(MTC_CA_EXT_SHA256_OID) != null; + } + + /** + * Extracts the CA Trust Anchor ID bytes (raw DER {@code RELATIVE-OID} content) from the + * trust anchor's Subject Distinguished Name per + * {@code draft-ietf-plants-merkle-tree-certs-06 Section 5.1}. + * + *

The Subject DN must consist of a single {@code RelativeDistinguishedName} containing a + * single {@code AttributeTypeAndValue} with type {@code id-rdna-trustAnchorID} + * ({@code 1.3.6.1.4.1.44363.47.3}) and value encoded as an ASN.1 {@code RELATIVE-OID}. + */ + public static byte[] extractCaId(TrustAnchor anchor) throws CertificateException { + X509Certificate cert = (anchor != null) ? anchor.getTrustedCert() : null; + if (!isMtcCaCertificate(cert)) { + throw new CertificateException("Trust anchor is not a valid MTC CA certificate"); + } + X500Principal principal = cert.getSubjectX500Principal(); + if (principal == null) { + throw new CertificateException("No subject principal found for MTC trust anchor"); + } + try { + ByteBuffer nameBuf = ByteBuffer.wrap(principal.getEncoded()); + ByteBuffer rdnSeq = DerUtils.readSequence(nameBuf); + if (nameBuf.hasRemaining()) { + throw new CertificateException("Trailing data outside Subject RDNSequence"); + } + ByteBuffer rdnSet = DerUtils.readSet(rdnSeq); + if (rdnSeq.hasRemaining()) { + throw new CertificateException( + "MTC CA Subject must contain a single RelativeDistinguishedName"); + } + ByteBuffer attrSeq = DerUtils.readSequence(rdnSet); + if (rdnSet.hasRemaining()) { + throw new CertificateException( + "MTC CA Subject RDN must contain a single AttributeTypeAndValue"); + } + String attrOid = DerUtils.readOid(attrSeq); + if (!MTC_CA_ID_OID.equals(attrOid)) { + throw new CertificateException("Unexpected MTC CA Subject attribute OID: " + + attrOid); + } + byte[] caId = DerUtils.readRelativeOidBytes(attrSeq); + if (attrSeq.hasRemaining()) { + throw new CertificateException( + "Trailing data in MTC CA Subject AttributeTypeAndValue"); + } + return caId; + } catch (SerializationException e) { + throw new CertificateException("Failed to parse MTC CA Subject DN", e); + } + } + + /** + * Validates that the leaf certificate's serial number falls within the allowed + * {@code [minSerial, maxSerial]} range encoded in the trust anchor's critical + * {@code id-pe-mtcCertificationAuthority-SHA256} extension (Section 5.5 and Section 7.2 Step + * 4). + */ + public static void validateSerialInCaRange(TrustAnchor anchor, BigInteger serial) + throws CertificateException { + try { + ByteBuffer seqBuf = readCaExtensionSequence(anchor); + DerUtils.skipSequence(seqBuf); // sigAlg AlgorithmIdentifier + BigInteger minSerial = DerUtils.readBigInteger(seqBuf); + BigInteger maxSerial = DerUtils.readBigInteger(seqBuf); + if (seqBuf.hasRemaining()) { + throw new CertificateException( + "Trailing data in mtcCertificationAuthority extension"); + } + if (minSerial.compareTo(MIN_MTC_SERIAL) < 0 || maxSerial.compareTo(MAX_MTC_SERIAL) > 0 + || minSerial.compareTo(maxSerial) > 0) { + throw new CertificateException("Invalid [minSerial, maxSerial] bounds in MTC CA"); + } + if (serial.compareTo(minSerial) < 0 || serial.compareTo(maxSerial) > 0) { + throw new CertificateException("MTC certificate serial " + serial + + " is outside CA allowed range [" + minSerial + + ", " + maxSerial + "]"); + } + } catch (SerializationException e) { + throw new CertificateException("Failed to parse mtcCertificationAuthority extension", + e); + } + } + + /** + * Extracts the signature algorithm OID string ({@code sigAlg.algorithm}) from the trust + * anchor's critical {@code id-pe-mtcCertificationAuthority-SHA256} extension per + * {@code draft-ietf-plants-merkle-tree-certs-06 Section 5.5}. + * + *

Only ML-DSA algorithms ({@code ML-DSA-44}, {@code ML-DSA-65}, {@code ML-DSA-87}) with + * omitted {@code AlgorithmIdentifier} parameters (RFC 9881 Section 2) are supported. + */ + public static String extractCaSigAlg(TrustAnchor anchor) throws CertificateException { + try { + ByteBuffer seqBuf = readCaExtensionSequence(anchor); + ByteBuffer sigAlgSeq = DerUtils.readSequence(seqBuf); + String sigAlgOid = DerUtils.readOid(sigAlgSeq); + // We only support ML-DSA signature algorithms for MTC CA subtree root signatures. + // Per RFC 9881 Section 2, the AlgorithmIdentifier parameters field MUST be absent. + if (!isMlDsaOid(sigAlgOid)) { + throw new CertificateException("Unsupported MTC CA signature algorithm OID: " + + sigAlgOid); + } + if (sigAlgSeq.hasRemaining()) { + throw new CertificateException( + "Unexpected parameters in MTC CA sigAlg AlgorithmIdentifier"); + } + return sigAlgOid; + } catch (SerializationException e) { + throw new CertificateException("Failed to parse mtcCertificationAuthority extension", + e); + } + } + + /** + * Returns {@code true} if the given OID identifies a supported ML-DSA signature algorithm + * ({@code ML-DSA-44}, {@code ML-DSA-65}, or {@code ML-DSA-87} per RFC 9881 Section 2). + * Only ML-DSA algorithms are supported for MTC CA signatures. + */ + private static boolean isMlDsaOid(String oid) { + return ML_DSA_44_OID.equals(oid) || ML_DSA_65_OID.equals(oid) || ML_DSA_87_OID.equals(oid); + } + + private static ByteBuffer readCaExtensionSequence(TrustAnchor anchor) + throws CertificateException, SerializationException { + X509Certificate caCert = (anchor != null) ? anchor.getTrustedCert() : null; + if (caCert == null) { + throw new CertificateException("No X509Certificate found in MTC trust anchor"); + } + Set criticalOids = caCert.getCriticalExtensionOIDs(); + if (criticalOids == null || !criticalOids.contains(MTC_CA_EXT_SHA256_OID)) { + throw new CertificateException( + "MTC CA certificate is missing critical mtcCertificationAuthority extension"); + } + byte[] extValue = caCert.getExtensionValue(MTC_CA_EXT_SHA256_OID); + if (extValue == null) { + throw new CertificateException( + "MTC CA certificate is missing mtcCertificationAuthority extension value"); + } + ByteBuffer extBuf = ByteBuffer.wrap(DerUtils.readOctetString(extValue)); + ByteBuffer seqBuf = DerUtils.readSequence(extBuf); + if (extBuf.hasRemaining()) { + throw new CertificateException( + "Trailing data outside mtcCertificationAuthority SEQUENCE"); + } + return seqBuf; + } + + /** + * Computes the Subtree v1 Log ID bytes ({@code .0.}) as a base-128 + * {@code RELATIVE-OID} byte array per draft-ietf-plants-merkle-tree-certs-06 Section 5.2. + * + * @param caId the issuing CA Trust Anchor ID bytes + * @param logNumber the log number from the MTC proof + * @return the encoded Log ID bytes ({@code .0.}) + */ + public static byte[] getLogId(byte[] caId, int logNumber) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(caId.length + 4); + baos.write(caId, 0, caId.length); + baos.write(0); + appendBase128(baos, logNumber); + return baos.toByteArray(); + } + + private static void appendBase128(ByteArrayOutputStream out, long val) { + if (val < 0) { + throw new IllegalArgumentException("Base128 value must be non-negative: " + val); + } + // Collect 7-bit chunks from least-significant to most-significant into buf: + // buf[0] holds the final (least-significant) byte with continuation bit 0, and + // buf[1..count-1] hold the preceding bytes with continuation bit 0x80 set. + byte[] buf = new byte[10]; + int count = 0; + buf[count++] = (byte) (val & 0x7F); + val >>>= 7; + while (val > 0) { + buf[count++] = (byte) ((val & 0x7F) | 0x80); + val >>>= 7; + } + // Emit the collected bytes in reverse order so the output is big-endian. + for (int i = count - 1; i >= 0; i--) { + out.write(buf[i]); + } + } + + /** + * Constructs the CosignedMessage structure for signature verification, per + * draft-ietf-plants-merkle-tree-certs-06 Section 5.3.1 and Section 7.2 Step 12. + */ + private static byte[] constructCosignedMessage(byte[] cosignerId, byte[] logId, long start, + long end, byte[] expectedSubtreeHash) + throws CertificateException { + String cosignerIdText; + String logIdText; + try { + cosignerIdText = DerUtils.formatRelativeOid(cosignerId); + logIdText = DerUtils.formatRelativeOid(logId); + } catch (SerializationException e) { + throw new CertificateException("Invalid Trust Anchor or Log ID bytes", e); + } + byte[] cosignerName = (MTC_TAI_PREFIX + cosignerIdText).getBytes(StandardCharsets.UTF_8); + byte[] logOrigin = (MTC_TAI_PREFIX + logIdText).getBytes(StandardCharsets.UTF_8); + + int totalLen = MTC_SUBTREE_V1_LABEL.length + 1 + cosignerName.length + + 8 // timestamp (uint64 0) + + 1 + logOrigin.length + 8 // start (uint64) + + 8 // end (uint64) + + expectedSubtreeHash.length; + + ByteBuffer buf = ByteBuffer.allocate(totalLen); + buf.put(MTC_SUBTREE_V1_LABEL); + buf.put((byte) cosignerName.length); + buf.put(cosignerName); + buf.putLong(0L); // timestamp = 0 + buf.put((byte) logOrigin.length); + buf.put(logOrigin); + buf.putLong(start); + buf.putLong(end); + buf.put(expectedSubtreeHash); + return buf.array(); + } + + /** + * Finds and extracts the signature bytes for the specified cosigner / issuing CA Trust Anchor + * ID from the signatures payload, while validating that all {@code cosigner_id} entries are + * non-empty, unique, and strictly ordered (shorter byte strings first, then lexicographically) + * per Section 6.2. + */ + public static byte[] findCaSignature(byte[] signatures, byte[] caId) + throws CertificateException { + if (caId == null || caId.length == 0) { + throw new CertificateException("Empty CA Trust Anchor ID"); + } + ByteBuffer sigsBuf = ByteBuffer.wrap(signatures); + byte[] prevSignerId = null; + byte[] matchedSig = null; + while (sigsBuf.hasRemaining()) { + int signerIdLen = readUint8(sigsBuf); + if (signerIdLen == 0) { + throw new CertificateException("Malformed MTC proof: empty cosigner_id"); + } + byte[] signerIdBytes = readBytes(sigsBuf, signerIdLen); + if (prevSignerId != null && compareCosignerIds(prevSignerId, signerIdBytes) >= 0) { + throw new CertificateException( + "Malformed MTC proof: cosigner_id values are duplicate or out of order"); + } + prevSignerId = signerIdBytes; + + int sigLen = readUint16(sigsBuf); + byte[] sigBytes = readBytes(sigsBuf, sigLen); + + if (Arrays.equals(signerIdBytes, caId)) { + matchedSig = sigBytes; + } + } + return matchedSig; + } + + /** + * Compares two {@code cosigner_id} byte arrays per Section 6.2: shorter byte strings precede + * longer byte strings, and equal-length byte strings are ordered lexicographically (unsigned). + */ + private static int compareCosignerIds(byte[] a, byte[] b) { + if (a.length != b.length) { + return Integer.compare(a.length, b.length); + } + for (int i = 0; i < a.length; i++) { + int cmp = Integer.compare(Byte.toUnsignedInt(a[i]), Byte.toUnsignedInt(b[i])); + if (cmp != 0) { + return cmp; + } + } + return 0; + } + + /** + * Verifies the issuing CA's signature over the signed tree root for the specified log ID. + * + * @throws CertificateException if the signature is invalid + */ + public static void verifySignedTreeRoot(PublicKey publicKey, String sigAlg, byte[] caId, + byte[] logId, long start, long end, + byte[] expectedSubtreeHash, byte[] caSignature) + throws CertificateException { + byte[] msg = constructCosignedMessage(caId, logId, start, end, expectedSubtreeHash); + try { + verifySignatureWithKey(publicKey, sigAlg, msg, caSignature); + } catch (Exception e) { + throw new CertificateException("MTC CA signed tree root verification failed", e); + } + } + + /** + * Reads an 8-bit unsigned integer from the buffer. + */ + private static int readUint8(ByteBuffer buffer) throws CertificateException { + if (buffer.remaining() < 1) { + throw new CertificateException("Malformed MTC proof: unexpected end of data"); + } + return Byte.toUnsignedInt(buffer.get()); + } + + /** + * Reads a 16-bit unsigned integer from the buffer in big-endian order. + */ + public static int readUint16(ByteBuffer buffer) throws CertificateException { + if (buffer.remaining() < 2) { + throw new CertificateException("Malformed MTC proof: unexpected end of data"); + } + return Short.toUnsignedInt(buffer.getShort()); + } + + /** + * Reads a 48-bit unsigned integer from the buffer in big-endian order. + */ + public static long readUint48(ByteBuffer buffer) throws CertificateException { + if (buffer.remaining() < 6) { + throw new CertificateException("Malformed MTC proof: unexpected end of data"); + } + return (Integer.toUnsignedLong(buffer.getInt()) << 16) + | Short.toUnsignedInt(buffer.getShort()); + } + + /** + * Reads a byte array of the specified length from the buffer. + */ + public static byte[] readBytes(ByteBuffer buffer, int length) throws CertificateException { + if (buffer.remaining() < length) { + throw new CertificateException("Malformed MTC proof: unexpected end of data"); + } + byte[] bytes = new byte[length]; + buffer.get(bytes); + return bytes; + } + + /** + * Obtains a thread-safe / clean instance of SHA-256 {@link MessageDigest}. + */ + public static MessageDigest getSha256Digest() throws CertificateException { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new CertificateException("SHA-256 algorithm not found", e); + } + } + + /** + * Computes the SHA-256 digest of the given data. + */ + public static byte[] sha256(byte[] data) throws CertificateException { + return getSha256Digest().digest(data); + } + + /** + * Verifies a digital signature against the provided data using the specified public key and + * signature algorithm. + */ + private static void verifySignatureWithKey(PublicKey publicKey, String sigAlg, byte[] data, + byte[] signatureBytes) throws Exception { + Signature sig = Signature.getInstance(sigAlg); + sig.initVerify(publicKey); + sig.update(data); + if (!sig.verify(signatureBytes)) { + throw new SignatureException("Signature verification returned false for alg " + sigAlg); + } + } + + /** + * Finds the trust anchor for the issuing CA of the specified MTC leaf certificate. + * + * @param leaf the MTC leaf certificate whose issuing CA is being looked up + * @param acceptedIssuers array of accepted issuer certificates, or {@code null} + * @param certStore the trusted certificate store, or {@code null} + * @return the matching {@link TrustAnchor}, or {@code null} if not found + */ + public static TrustAnchor findIssuingCaTrustAnchor(X509Certificate leaf, + X509Certificate[] acceptedIssuers, + ConscryptCertStore certStore) { + X500Principal issuer = leaf.getIssuerX500Principal(); + if (issuer == null) { + return null; + } + + // 1. Check acceptedIssuers if available + if (acceptedIssuers != null && acceptedIssuers.length > 0) { + for (X509Certificate cert : acceptedIssuers) { + if (issuer.equals(cert.getSubjectX500Principal()) && isMtcCaCertificate(cert)) { + return new TrustAnchor(cert, null); + } + } + } + + // 2. Query certStore lazily (e.g. AndroidCAStore / NetworkSecurityConfig) + if (certStore != null) { + Set storeAnchors = certStore.findAllIssuers(leaf); + if (storeAnchors != null) { + for (X509Certificate cert : storeAnchors) { + if (isMtcCaCertificate(cert)) { + return new TrustAnchor(cert, null); + } + } + } + } + + return null; + } + + /** + * Checks whether the interval {@code [start, end)} defines a valid subtree per + * draft-ietf-plants-merkle-tree-certs-06 Section 4.1: + *

    + *
  • {@code 0 <= start <= end <= 2^48}
  • + *
  • {@code start} is a multiple of {@code BIT_CEIL(end - start)}
  • + *
+ * + *

In the MTC protocol, {@code start} and {@code end} are 48-bit unsigned integers + * ({@code uint48} in {@code [0, 2^48 - 1]}), so they are always non-negative when represented + * as signed 64-bit {@code long} values and {@code end - start} cannot overflow. + */ + public static boolean isValidSubtree(long start, long end) { + if (start < 0 || start > end || end > (1L << MTC_INDEX_BITS)) { + return false; + } + long size = end - start; + long bitCeil = (size <= 1) ? 1L : (1L << (64 - Long.numberOfLeadingZeros(size - 1))); + return (start & (bitCeil - 1)) == 0; + } +} diff --git a/common/src/main/java/org/conscrypt/ct/SerializationException.java b/common/src/main/java/org/conscrypt/util/SerializationException.java similarity index 92% rename from common/src/main/java/org/conscrypt/ct/SerializationException.java rename to common/src/main/java/org/conscrypt/util/SerializationException.java index 833dd6979..aea9225f7 100644 --- a/common/src/main/java/org/conscrypt/ct/SerializationException.java +++ b/common/src/main/java/org/conscrypt/util/SerializationException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 The Android Open Source Project + * Copyright (C) 2026 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.conscrypt.ct; +package org.conscrypt.util; import org.conscrypt.Internal; diff --git a/common/src/test/java/org/conscrypt/HkdfTest.java b/common/src/test/java/org/conscrypt/HkdfTest.java index cb2fe3e75..688eb4147 100644 --- a/common/src/test/java/org/conscrypt/HkdfTest.java +++ b/common/src/test/java/org/conscrypt/HkdfTest.java @@ -36,6 +36,7 @@ public void constructor_invalidHmacAlgorithm_throws() throws Exception { assertThrows(NullPointerException.class, () -> new Hkdf(null)); assertThrows(NoSuchAlgorithmException.class, () -> new Hkdf("No such MAC")); assertThrows(NoSuchAlgorithmException.class, () -> new Hkdf("AESCMAC")); + assertThrows(NoSuchAlgorithmException.class, () -> new Hkdf("SHA256")); } @Test diff --git a/common/src/test/java/org/conscrypt/KeyManagerFactoryImplTest.java b/common/src/test/java/org/conscrypt/KeyManagerFactoryImplTest.java new file mode 100644 index 000000000..656857e1f --- /dev/null +++ b/common/src/test/java/org/conscrypt/KeyManagerFactoryImplTest.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 org.conscrypt; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import org.conscrypt.java.security.TestKeyStore; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.KeyStoreSpi; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.util.Date; +import java.util.Enumeration; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.ManagerFactoryParameters; + +@RunWith(JUnit4.class) +public class KeyManagerFactoryImplTest { + private TestKeyStore testKeyStore; + + @Before + public void setUp() { + testKeyStore = TestKeyStore.getClient(); + } + + private TestKeyStore getTestKeyStore() { + return testKeyStore; + } + + @Test + public void test_KeyManagerFactory_Conscrypt() throws Exception { + KeyManagerFactory kmf = + new KeyManagerFactory(new KeyManagerFactoryImpl(), TestUtils.getConscryptProvider(), + KeyManagerFactory.getDefaultAlgorithm()) {}; + test_KeyManagerFactory(kmf); + + // Test that using a KeyStore that doesn't implement getEntry(), like Android Keystore + // doesn't, still produces a functional KeyManager. + kmf.init(new NoGetEntryKeyStore(getTestKeyStore().keyStore), + getTestKeyStore().storePassword); + test_KeyManagerFactory_getKeyManagers(kmf, false); + } + + private void test_KeyManagerFactory(KeyManagerFactory kmf) throws Exception { + try { + kmf.getKeyManagers(); + fail(); + } catch (IllegalStateException expected) { + } + + kmf.init(null, null); + test_KeyManagerFactory_getKeyManagers(kmf, true); + + kmf.init(getTestKeyStore().keyStore, null); + test_KeyManagerFactory_getKeyManagers(kmf, true); + + kmf.init(getTestKeyStore().keyStore, getTestKeyStore().storePassword); + test_KeyManagerFactory_getKeyManagers(kmf, false); + + try { + kmf.init((ManagerFactoryParameters) null); + fail(); + } catch (InvalidAlgorithmParameterException expected) { + } + } + + private static void test_KeyManagerFactory_getKeyManagers(KeyManagerFactory kmf, + boolean isDefault) { + KeyManager[] keyManagers = kmf.getKeyManagers(); + assertNotNull(keyManagers); + for (KeyManager keyManager : keyManagers) { + assertNotNull(keyManager); + } + } + + private static class NoGetEntryKeyStore extends KeyStore { + public NoGetEntryKeyStore(KeyStore keyStore) throws Exception { + super(new NoGetEntryKeyStoreSpi(keyStore), keyStore.getProvider(), keyStore.getType()); + load(null, null); + } + } + + private static class NoGetEntryKeyStoreSpi extends KeyStoreSpi { + private final KeyStore keyStore; + + public NoGetEntryKeyStoreSpi(KeyStore keyStore) { + this.keyStore = keyStore; + } + + @Override + public KeyStore.Entry engineGetEntry(String alias, KeyStore.ProtectionParameter protParam) { + throw new UnsupportedOperationException(); + } + + @Override + public Key engineGetKey(String s, char[] chars) + throws NoSuchAlgorithmException, UnrecoverableKeyException { + try { + return keyStore.getKey(s, chars); + } catch (KeyStoreException e) { + throw new AssertionError(e); + } + } + + @Override + public Certificate[] engineGetCertificateChain(String s) { + try { + return keyStore.getCertificateChain(s); + } catch (KeyStoreException e) { + throw new AssertionError(e); + } + } + + @Override + public Certificate engineGetCertificate(String s) { + try { + return keyStore.getCertificate(s); + } catch (KeyStoreException e) { + throw new AssertionError(e); + } + } + + @Override + public Date engineGetCreationDate(String s) { + try { + return keyStore.getCreationDate(s); + } catch (KeyStoreException e) { + throw new AssertionError(e); + } + } + + @Override + public void engineSetKeyEntry(String s, Key key, char[] chars, Certificate[] certificates) + throws KeyStoreException { + try { + keyStore.setKeyEntry(s, key, chars, certificates); + } catch (KeyStoreException e) { + throw new AssertionError(e); + } + } + + @Override + public void engineSetKeyEntry(String s, byte[] bytes, Certificate[] certificates) + throws KeyStoreException { + try { + keyStore.setKeyEntry(s, bytes, certificates); + } catch (KeyStoreException e) { + throw new AssertionError(e); + } + } + + @Override + public void engineSetCertificateEntry(String s, Certificate certificate) + throws KeyStoreException { + try { + keyStore.setCertificateEntry(s, certificate); + } catch (KeyStoreException e) { + throw new AssertionError(e); + } + } + + @Override + public void engineDeleteEntry(String s) throws KeyStoreException { + try { + keyStore.deleteEntry(s); + } catch (KeyStoreException e) { + throw new AssertionError(e); + } + } + + @Override + public Enumeration engineAliases() { + try { + return keyStore.aliases(); + } catch (KeyStoreException e) { + throw new AssertionError(e); + } + } + + @Override + public boolean engineContainsAlias(String s) { + try { + return keyStore.containsAlias(s); + } catch (KeyStoreException e) { + throw new AssertionError(e); + } + } + + @Override + public int engineSize() { + try { + return keyStore.size(); + } catch (KeyStoreException e) { + throw new AssertionError(e); + } + } + + @Override + public boolean engineIsKeyEntry(String s) { + try { + return keyStore.isKeyEntry(s); + } catch (KeyStoreException e) { + throw new AssertionError(e); + } + } + + @Override + public boolean engineIsCertificateEntry(String s) { + try { + return keyStore.isCertificateEntry(s); + } catch (KeyStoreException e) { + throw new AssertionError(e); + } + } + + @Override + public String engineGetCertificateAlias(Certificate certificate) { + try { + return keyStore.getCertificateAlias(certificate); + } catch (KeyStoreException e) { + throw new AssertionError(e); + } + } + + @Override + public void engineStore(OutputStream outputStream, char[] chars) + throws IOException, NoSuchAlgorithmException, CertificateException { + try { + keyStore.store(outputStream, chars); + } catch (KeyStoreException e) { + throw new AssertionError(e); + } + } + + @Override + public void engineLoad(InputStream inputStream, char[] chars) + throws IOException, NoSuchAlgorithmException, CertificateException { + // Do nothing, the keystore is already loaded + } + } +} diff --git a/common/src/test/java/org/conscrypt/ct/SerializationTest.java b/common/src/test/java/org/conscrypt/ct/SerializationTest.java index 9151fa49c..5b22cd123 100644 --- a/common/src/test/java/org/conscrypt/ct/SerializationTest.java +++ b/common/src/test/java/org/conscrypt/ct/SerializationTest.java @@ -22,6 +22,8 @@ // android-add: import libcore.test.annotation.NonCts; // android-add: import libcore.test.reasons.NonCtsReasons; + +import org.conscrypt.util.SerializationException; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -194,52 +196,6 @@ public void test_encode_CertificateEntry_PreCertificate() throws Exception { output.toByteArray()); } - @Test - // android-add: @NonCts(reason = NonCtsReasons.INTERNAL_APIS) - public void test_readDEROctetString() throws Exception { - byte[] in, expected; - - in = new byte[] { - 0x04, // TAG - 0x06, // length - 0x01, 0x02, 0x03, 0x04, 0x05, 0x05 // data - }; - expected = new byte[] {0x01, 0x02, 0x03, 0x04, 0x05, 0x05}; - assertEqualByteArrays(expected, Serialization.readDEROctetString(in)); - - in = new byte[203]; - in[0] = 0x04; // TAG - in[1] = (byte) 0x81; // long length flag - in[2] = (byte) 200; // length - in[3] = 0x45; // data, the rest is just zeros - - expected = new byte[200]; - expected[0] = 0x45; - assertEqualByteArrays(expected, Serialization.readDEROctetString(in)); - - try { - in = new byte[] { - 0x12, // wrong tag - 0x06, // length - 0x01, 0x02, 0x03, 0x04, 0x05, 0x05 // data - }; - Serialization.readDEROctetString(in); - fail("SerializationException not thrown on invalid tag."); - } catch (SerializationException e) { - } - - try { - in = new byte[] { - 0x04, // wrong tag - 0x06, // length - 0x01, 0x02 // data - }; - Serialization.readDEROctetString(in); - fail("SerializationException not thrown on invalid length."); - } catch (SerializationException e) { - } - } - public static void assertEqualByteArrays(byte[] expected, byte[] actual) { assertEquals(Arrays.toString(expected), Arrays.toString(actual)); } diff --git a/common/src/test/java/org/conscrypt/java/security/cert/CertificateFactoryTest.java b/common/src/test/java/org/conscrypt/java/security/cert/CertificateFactoryTest.java index d778645db..eae45b50e 100644 --- a/common/src/test/java/org/conscrypt/java/security/cert/CertificateFactoryTest.java +++ b/common/src/test/java/org/conscrypt/java/security/cert/CertificateFactoryTest.java @@ -31,7 +31,6 @@ import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.SubjectKeyIdentifier; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; -import org.conscrypt.Conscrypt; import org.conscrypt.TestUtils; import org.conscrypt.java.security.StandardNames; // android-add: import org.junit.ClassRule; @@ -664,7 +663,7 @@ public void testGenerateCertPath() throws Exception { for (Provider p : providers) { final CertificateFactory cf = CertificateFactory.getInstance("X.509", p); - if (Conscrypt.isConscrypt(p)) { + if (TestUtils.isConscrypt(p)) { // It's not specified whether duplicated certs should work, but we want Conscrypt // to accept them { diff --git a/common/src/test/java/org/conscrypt/javax/crypto/CipherTest.java b/common/src/test/java/org/conscrypt/javax/crypto/CipherTest.java index 9a7df9139..1ebf1de9a 100644 --- a/common/src/test/java/org/conscrypt/javax/crypto/CipherTest.java +++ b/common/src/test/java/org/conscrypt/javax/crypto/CipherTest.java @@ -31,7 +31,6 @@ // android-add: import libcore.test.reasons.NonCtsReasons; import org.bouncycastle.asn1.x509.KeyUsage; -import org.conscrypt.Conscrypt; import org.conscrypt.TestUtils; import org.conscrypt.java.security.StandardNames; import org.conscrypt.java.security.TestKeyStore; @@ -1088,7 +1087,7 @@ public void test_getInstance() throws Exception { || baseCipherName.equals("XCHACHA20"))) { seenCiphersWithModeAndPadding.add(baseCipherName); } - if (!Conscrypt.isConscrypt(provider)) { + if (!TestUtils.isConscrypt(provider)) { continue; } } @@ -4773,7 +4772,7 @@ public void testDecryptBufferZeroSize_mustDecodeToEmptyString() throws Exception "DESEDE/CBC/PKCS5PADDING", "DESEDE/CBC/PKCS7PADDING"}; for (String c : androidOpenSSLCiphers) { Cipher cipher = Cipher.getInstance(c); - assertTrue(Conscrypt.isConscrypt(cipher.getProvider())); + assertTrue(TestUtils.isConscrypt(cipher.getProvider())); if (c.contains("/CBC/")) { cipher.init( Cipher.DECRYPT_MODE, diff --git a/common/src/test/java/org/conscrypt/javax/crypto/ECDHKeyAgreementTest.java b/common/src/test/java/org/conscrypt/javax/crypto/ECDHKeyAgreementTest.java index bd077d72c..f56f34fd7 100644 --- a/common/src/test/java/org/conscrypt/javax/crypto/ECDHKeyAgreementTest.java +++ b/common/src/test/java/org/conscrypt/javax/crypto/ECDHKeyAgreementTest.java @@ -27,7 +27,6 @@ import junit.framework.AssertionFailedError; -import org.conscrypt.Conscrypt; import org.conscrypt.TestUtils; import org.junit.BeforeClass; // android-add: import org.junit.ClassRule; @@ -390,7 +389,7 @@ void testGenerateSecret_withAlgorithm(Provider provider) throws Exception { assertTrue(Arrays.equals(KAT_SECRET, key.getEncoded())); } catch (NoSuchAlgorithmException e) { // This provider doesn't support AES, that's fine as long as it's not Conscrypt - assertFalse(Conscrypt.isConscrypt(provider)); + assertFalse(TestUtils.isConscrypt(provider)); } } diff --git a/common/src/test/java/org/conscrypt/javax/net/ssl/KeyManagerFactoryTest.java b/common/src/test/java/org/conscrypt/javax/net/ssl/KeyManagerFactoryTest.java index 0f8ad71c5..4c61215ce 100644 --- a/common/src/test/java/org/conscrypt/javax/net/ssl/KeyManagerFactoryTest.java +++ b/common/src/test/java/org/conscrypt/javax/net/ssl/KeyManagerFactoryTest.java @@ -24,7 +24,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import org.conscrypt.KeyManagerFactoryImpl; import org.conscrypt.TestUtils; import org.conscrypt.java.security.StandardNames; import org.conscrypt.java.security.TestKeyStore; @@ -354,189 +353,4 @@ public void test(Provider provider, String algorithm) throws Exception { } }); } - - // The Conscrypt provider on OpenJDK doesn't provide the KeyManagerFactory, but we want - // to test it on OpenJDK anyway - @Test - public void test_KeyManagerFactory_Conscrypt() throws Exception { - KeyManagerFactory kmf = - new KeyManagerFactory(new KeyManagerFactoryImpl(), TestUtils.getConscryptProvider(), - KeyManagerFactory.getDefaultAlgorithm()) {}; - test_KeyManagerFactory(kmf); - - // Test that using a KeyStore that doesn't implement getEntry(), like Android Keystore - // doesn't, still produces a functional KeyManager. - kmf.init(new NoGetEntryKeyStore(getTestKeyStore().keyStore), - getTestKeyStore().storePassword); - test_KeyManagerFactory_getKeyManagers(kmf, false); - } - - private static class NoGetEntryKeyStore extends KeyStore { - public NoGetEntryKeyStore(KeyStore keyStore) throws Exception { - super(new NoGetEntryKeyStoreSpi(keyStore), keyStore.getProvider(), keyStore.getType()); - load(null, null); - } - } - - // Android Keystore's KeyStore doesn't support getEntry(), so we replicate that here - // for testing by throwing UnsupportedOperationException and passing everything else through - // to a working implementation. - private static class NoGetEntryKeyStoreSpi extends KeyStoreSpi { - private final KeyStore keyStore; - - public NoGetEntryKeyStoreSpi(KeyStore keyStore) { - this.keyStore = keyStore; - } - - @Override - public KeyStore.Entry engineGetEntry(String alias, KeyStore.ProtectionParameter protParam) { - throw new UnsupportedOperationException(); - } - - @Override - public Key engineGetKey(String s, char[] chars) - throws NoSuchAlgorithmException, UnrecoverableKeyException { - try { - return keyStore.getKey(s, chars); - } catch (KeyStoreException e) { - throw new AssertionError(e); - } - } - - @Override - public Certificate[] engineGetCertificateChain(String s) { - try { - return keyStore.getCertificateChain(s); - } catch (KeyStoreException e) { - throw new AssertionError(e); - } - } - - @Override - public Certificate engineGetCertificate(String s) { - try { - return keyStore.getCertificate(s); - } catch (KeyStoreException e) { - throw new AssertionError(e); - } - } - - @Override - public Date engineGetCreationDate(String s) { - try { - return keyStore.getCreationDate(s); - } catch (KeyStoreException e) { - throw new AssertionError(e); - } - } - - @Override - public void engineSetKeyEntry(String s, Key key, char[] chars, Certificate[] certificates) - throws KeyStoreException { - try { - keyStore.setKeyEntry(s, key, chars, certificates); - } catch (KeyStoreException e) { - throw new AssertionError(e); - } - } - - @Override - public void engineSetKeyEntry(String s, byte[] bytes, Certificate[] certificates) - throws KeyStoreException { - try { - keyStore.setKeyEntry(s, bytes, certificates); - } catch (KeyStoreException e) { - throw new AssertionError(e); - } - } - - @Override - public void engineSetCertificateEntry(String s, Certificate certificate) - throws KeyStoreException { - try { - keyStore.setCertificateEntry(s, certificate); - } catch (KeyStoreException e) { - throw new AssertionError(e); - } - } - - @Override - public void engineDeleteEntry(String s) throws KeyStoreException { - try { - keyStore.deleteEntry(s); - } catch (KeyStoreException e) { - throw new AssertionError(e); - } - } - - @Override - public Enumeration engineAliases() { - try { - return keyStore.aliases(); - } catch (KeyStoreException e) { - throw new AssertionError(e); - } - } - - @Override - public boolean engineContainsAlias(String s) { - try { - return keyStore.containsAlias(s); - } catch (KeyStoreException e) { - throw new AssertionError(e); - } - } - - @Override - public int engineSize() { - try { - return keyStore.size(); - } catch (KeyStoreException e) { - throw new AssertionError(e); - } - } - - @Override - public boolean engineIsKeyEntry(String s) { - try { - return keyStore.isKeyEntry(s); - } catch (KeyStoreException e) { - throw new AssertionError(e); - } - } - - @Override - public boolean engineIsCertificateEntry(String s) { - try { - return keyStore.isCertificateEntry(s); - } catch (KeyStoreException e) { - throw new AssertionError(e); - } - } - - @Override - public String engineGetCertificateAlias(Certificate certificate) { - try { - return keyStore.getCertificateAlias(certificate); - } catch (KeyStoreException e) { - throw new AssertionError(e); - } - } - - @Override - public void engineStore(OutputStream outputStream, char[] chars) - throws IOException, NoSuchAlgorithmException, CertificateException { - try { - keyStore.store(outputStream, chars); - } catch (KeyStoreException e) { - throw new AssertionError(e); - } - } - - @Override - public void engineLoad(InputStream inputStream, char[] chars) - throws IOException, NoSuchAlgorithmException, CertificateException { - // Do nothing, the keystore is already loaded - } - } } diff --git a/common/src/test/java/org/conscrypt/javax/net/ssl/SSLEngineVersionCompatibilityTest.java b/common/src/test/java/org/conscrypt/javax/net/ssl/SSLEngineVersionCompatibilityTest.java index acf74af85..10aa19f75 100644 --- a/common/src/test/java/org/conscrypt/javax/net/ssl/SSLEngineVersionCompatibilityTest.java +++ b/common/src/test/java/org/conscrypt/javax/net/ssl/SSLEngineVersionCompatibilityTest.java @@ -499,39 +499,6 @@ public void test_SSLEngine_Closed() throws Exception { } } - @Test - public void test_SSLEngine_ClientHello_record_size() throws Exception { - // This test checks the size of ClientHello of the default SSLEngine. TLS/SSL handshakes - // with older/unpatched F5/BIG-IP appliances are known to stall and time out when - // the fragment containing ClientHello is between 256 and 511 (inclusive) bytes long. - SSLContext context = SSLContext.getInstance(clientVersion); - context.init(null, null, null); - SSLEngine e = context.createSSLEngine(); - e.setUseClientMode(true); - - // Enable SNI extension on the engine (this is typically enabled by default) - // to increase the size of ClientHello. - Conscrypt.setHostname(e, "sslenginetest.androidcts.google.com"); - - // Enable Session Tickets extension on the engine (this is typically enabled - // by default) to increase the size of ClientHello. - Conscrypt.setUseSessionTickets(e, true); - - TlsRecord firstReceivedTlsRecord = TlsTester.parseRecord(getFirstChunk(e)); - - assertEquals("TLS record type", TlsProtocols.HANDSHAKE, firstReceivedTlsRecord.type); - HandshakeMessage handshakeMessage = HandshakeMessage.read( - new DataInputStream(new ByteArrayInputStream(firstReceivedTlsRecord.fragment))); - assertEquals("HandshakeMessage type", HandshakeMessage.TYPE_CLIENT_HELLO, - handshakeMessage.type); - - int fragmentLength = firstReceivedTlsRecord.fragment.length; - if ((fragmentLength >= 256) && (fragmentLength <= 511)) { - fail("Fragment containing ClientHello is of dangerous length: " + fragmentLength - + " bytes"); - } - } - @Test public void test_SSLEngine_ClientHello_SNI() throws Exception { SSLContext context = SSLContext.getInstance(clientVersion); @@ -585,6 +552,8 @@ private static byte[] getFirstChunk(SSLEngine e) throws SSLException { public void test_SSLEngine_TlsUnique() throws Exception { // tls_unique isn't supported in TLS 1.3 assumeTlsV1_2Connection(); + SSLEngine clientEngine; + SSLEngine serverEngine; try (TestSSLContext c = TestSSLContext.newBuilder() .clientProtocol(clientVersion) .serverProtocol(serverVersion) @@ -597,6 +566,8 @@ void beforeBeginHandshake(SSLEngine client, SSLEngine server) { } })) { assertConnected(pair); + clientEngine = pair.client; + serverEngine = pair.server; byte[] clientTlsUnique = Conscrypt.getTlsUnique(pair.client); byte[] serverTlsUnique = Conscrypt.getTlsUnique(pair.server); @@ -604,10 +575,14 @@ void beforeBeginHandshake(SSLEngine client, SSLEngine server) { assertNotNull(serverTlsUnique); assertArrayEquals(clientTlsUnique, serverTlsUnique); } + assertNull(Conscrypt.getTlsUnique(clientEngine)); + assertNull(Conscrypt.getTlsUnique(serverEngine)); } @Test public void test_SSLEngine_EKM() throws Exception { + SSLEngine clientEngine; + SSLEngine serverEngine; try (TestSSLContext c = TestSSLContext.newBuilder() .clientProtocol(clientVersion) .serverProtocol(serverVersion) @@ -624,6 +599,8 @@ void beforeBeginHandshake(SSLEngine client, SSLEngine server) { } })) { assertConnected(pair); + clientEngine = pair.client; + serverEngine = pair.server; byte[] clientEkm = Conscrypt.exportKeyingMaterial(pair.client, "FOO", null, 20); byte[] serverEkm = Conscrypt.exportKeyingMaterial(pair.server, "FOO", null, 20); @@ -651,6 +628,8 @@ void beforeBeginHandshake(SSLEngine client, SSLEngine server) { assertTrue(Arrays.equals(clientEkm, clientContextEkm)); } } + assertNull(Conscrypt.exportKeyingMaterial(clientEngine, "FOO", null, 20)); + assertNull(Conscrypt.exportKeyingMaterial(serverEngine, "FOO", null, 20)); } // Test whether an exception thrown from within the TrustManager properly flows immediately diff --git a/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSessionContextTest.java b/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSessionContextTest.java index 634d19f06..be3b96ef3 100644 --- a/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSessionContextTest.java +++ b/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSessionContextTest.java @@ -16,7 +16,7 @@ package org.conscrypt.javax.net.ssl; -import static org.conscrypt.Conscrypt.isConscrypt; +import static org.conscrypt.TestUtils.isConscrypt; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; diff --git a/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSocketImplTest.java b/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSocketImplTest.java new file mode 100644 index 000000000..ac00a97e9 --- /dev/null +++ b/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSocketImplTest.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * 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 org.conscrypt.javax.net.ssl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import org.conscrypt.AllocatedBuffer; +import org.conscrypt.BufferAllocator; +import org.conscrypt.Conscrypt; +import org.conscrypt.OpenSSLSocketImpl; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +@RunWith(JUnit4.class) +public class SSLSocketImplTest { + private final ThreadGroup threadGroup = new ThreadGroup("SSLSocketImplTest"); + private final ExecutorService executor = + Executors.newCachedThreadPool(t -> new Thread(threadGroup, t)); + + @After + public void teardown() throws InterruptedException { + executor.shutdownNow(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + + String getCurveName(SSLSocket socket) { + return ((OpenSSLSocketImpl) socket).getCurveNameForTesting(); + } + + private Future runAsync(Callable callable) { + return executor.submit(callable); + } + + @Test + public void setBufferAllocator_afterConnect_fails() throws Exception { + TestSSLSocketPair pair = TestSSLSocketPair.create(); + BufferAllocator unpooledBufferAllocator = BufferAllocator.unpooled(); + pair.connect(); + assertThrows(IllegalStateException.class, + () -> Conscrypt.setBufferAllocator(pair.client, unpooledBufferAllocator)); + } + + // BufferAllocator that counts allocations. + private static final class SpyBufferAllocator extends BufferAllocator { + private final BufferAllocator bufferAllocator; + private int directAllocationCount = 0; + private int heapAllocationCount = 0; + + SpyBufferAllocator(BufferAllocator bufferAllocator) { + this.bufferAllocator = bufferAllocator; + } + + @Override + public AllocatedBuffer allocateDirectBuffer(int capacity) { + directAllocationCount++; + return bufferAllocator.allocateDirectBuffer(capacity); + } + + @Override + public AllocatedBuffer allocateHeapBuffer(int capacity) { + heapAllocationCount++; + return bufferAllocator.allocateHeapBuffer(capacity); + } + } + + @Test + public void setBufferAllocator_works() throws Exception { + TestSSLSocketPair pair = TestSSLSocketPair.create(); + SpyBufferAllocator clientBufferAllocator = + new SpyBufferAllocator(BufferAllocator.unpooled()); + SpyBufferAllocator serverBufferAllocator = + new SpyBufferAllocator(BufferAllocator.unpooled()); + Conscrypt.setBufferAllocator(pair.client, clientBufferAllocator); + Conscrypt.setBufferAllocator(pair.server, serverBufferAllocator); + pair.connect(); + + pair.client.getOutputStream().write(new byte[] {1, 2, 3, 4}); + assertEquals(1, pair.server.getInputStream().read()); + assertEquals(3, pair.server.getInputStream().read(new byte[4])); + + pair.server.getOutputStream().write(new byte[] {1, 2, 3, 4}); + assertEquals(1, pair.client.getInputStream().read()); + assertEquals(3, pair.client.getInputStream().read(new byte[4])); + + assertTrue(clientBufferAllocator.directAllocationCount > 0); + assertTrue(serverBufferAllocator.directAllocationCount > 0); + + // allocateHeapBuffer is currently not called. + assertEquals(0, clientBufferAllocator.heapAllocationCount); + assertEquals(0, serverBufferAllocator.heapAllocationCount); + } + + @Test + public void socket_setNamedGroups_works() throws Exception { + TestSSLContext context = TestSSLContext.create(); + final SSLSocket client = (SSLSocket) context.clientContext.getSocketFactory().createSocket( + context.host, context.port); + Conscrypt.setNamedGroups(client, new String[] {"P-384"}); + + // For the server, we don't set the named groups. P-384 should be + // enabled by default. + final SSLSocket server = (SSLSocket) context.serverSocket.accept(); + Future s = runAsync(() -> { + server.startHandshake(); + return null; + }); + Future c = runAsync(() -> { + client.startHandshake(); + return null; + }); + s.get(); + c.get(); + // This must works even if sslParametersSupportsNamedGroups is false. + assertEquals("P-384", getCurveName(client)); + assertEquals("P-384", getCurveName(server)); + client.close(); + server.close(); + context.close(); + } + + @Test + public void socketFactory_setNamedGroups_works() throws Exception { + TestSSLContext context = TestSSLContext.create(); + SSLSocketFactory clientSocketFactory = context.clientContext.getSocketFactory(); + Conscrypt.setNamedGroups(clientSocketFactory, new String[] {"P-384"}); + + final SSLSocket client = (SSLSocket) context.clientContext.getSocketFactory().createSocket( + context.host, context.port); + + // For the server, we don't set the named groups. P-384 should be + // enabled by default. + final SSLSocket server = (SSLSocket) context.serverSocket.accept(); + Future s = runAsync(() -> { + server.startHandshake(); + return null; + }); + Future c = runAsync(() -> { + client.startHandshake(); + return null; + }); + s.get(); + c.get(); + // This must works even if sslParametersSupportsNamedGroups is false. + assertEquals("P-384", getCurveName(client)); + assertEquals("P-384", getCurveName(server)); + client.close(); + server.close(); + context.close(); + } +} diff --git a/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSocketTest.java b/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSocketTest.java index 24e334b0c..d8a5b8de1 100644 --- a/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSocketTest.java +++ b/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSocketTest.java @@ -25,13 +25,10 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; import static java.nio.charset.StandardCharsets.UTF_8; -import org.conscrypt.AllocatedBuffer; -import org.conscrypt.BufferAllocator; -import org.conscrypt.Conscrypt; -import org.conscrypt.OpenSSLSocketImpl; import org.conscrypt.TestUtils; import org.conscrypt.java.security.StandardNames; import org.conscrypt.java.security.TestKeyStore; @@ -66,6 +63,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -100,8 +98,10 @@ public class SSLSocketTest { private static final TestSSLContext testSslContext = TestSSLContext.create(); - String getCurveName(SSLSocket socket) { - return ((OpenSSLSocketImpl) socket).getCurveNameForTesting(); + String getCurveName(SSLSocket socket) throws Exception { + Method getCurveNameMethod = socket.getClass().getMethod("getCurveNameForTesting"); + getCurveNameMethod.setAccessible(true); + return (String) getCurveNameMethod.invoke(socket); } /** @@ -272,65 +272,6 @@ private SecretKey newKey() { c.close(); } - @Test - public void setBufferAllocator_afterConnect_fails() throws Exception { - TestSSLSocketPair pair = TestSSLSocketPair.create(); - BufferAllocator unpooledBufferAllocator = BufferAllocator.unpooled(); - pair.connect(); - assertThrows(IllegalStateException.class, - () -> Conscrypt.setBufferAllocator(pair.client, unpooledBufferAllocator)); - } - - // BufferAllocator that counts allocations. - private static final class SpyBufferAllocator extends BufferAllocator { - private final BufferAllocator bufferAllocator; - private int directAllocationCount = 0; - private int heapAllocationCount = 0; - - SpyBufferAllocator(BufferAllocator bufferAllocator) { - this.bufferAllocator = bufferAllocator; - } - - @Override - public AllocatedBuffer allocateDirectBuffer(int capacity) { - directAllocationCount++; - return bufferAllocator.allocateDirectBuffer(capacity); - } - - @Override - public AllocatedBuffer allocateHeapBuffer(int capacity) { - heapAllocationCount++; - return bufferAllocator.allocateHeapBuffer(capacity); - } - } - - @Test - public void setBufferAllocator_works() throws Exception { - TestSSLSocketPair pair = TestSSLSocketPair.create(); - SpyBufferAllocator clientBufferAllocator = - new SpyBufferAllocator(BufferAllocator.unpooled()); - SpyBufferAllocator serverBufferAllocator = - new SpyBufferAllocator(BufferAllocator.unpooled()); - Conscrypt.setBufferAllocator(pair.client, clientBufferAllocator); - Conscrypt.setBufferAllocator(pair.server, serverBufferAllocator); - pair.connect(); - - pair.client.getOutputStream().write(new byte[] {1, 2, 3, 4}); - assertEquals(1, pair.server.getInputStream().read()); - assertEquals(3, pair.server.getInputStream().read(new byte[4])); - - pair.server.getOutputStream().write(new byte[] {1, 2, 3, 4}); - assertEquals(1, pair.client.getInputStream().read()); - assertEquals(3, pair.client.getInputStream().read(new byte[4])); - - assertTrue(clientBufferAllocator.directAllocationCount > 0); - assertTrue(serverBufferAllocator.directAllocationCount > 0); - - // allocateHeapBuffer is currently not called. - assertEquals(0, clientBufferAllocator.heapAllocationCount); - assertEquals(0, serverBufferAllocator.heapAllocationCount); - } - @Test public void test_SSLSocket_getInputStream_available() throws Exception { TestSSLSocketPair pair = TestSSLSocketPair.create().connect(); @@ -1113,6 +1054,171 @@ public void handshake_namedGroupsProperty_usesFirstKnownEntry() throws Exception context.close(); } + private static final long ENABLE_TLS_PQC_DEFAULT_GROUPS = 556729260L; + + private static void setCompatChangeEnabled(long changeId, boolean enabled) throws Exception { + try { + Class platformClass = + Class.forName(TestUtils.class.getPackage().getName() + ".Platform"); + platformClass.getMethod("setPqcDefaultEnabledForTesting", boolean.class) + .invoke(null, enabled); + return; + } catch (ReflectiveOperationException ignored) { + // Platform module uses android.compat.Compatibility overrides instead. + } + Class compatClass = Class.forName("android.compat.Compatibility"); + Class changeConfigClass = Class.forName("android.compat.Compatibility$ChangeConfig"); + try { + compatClass.getMethod("clearOverrides").invoke(null); + } catch (Throwable ignored) { + // No overrides were previously set. + } + Set enabledSet = + enabled ? Collections.singleton(changeId) : Collections.emptySet(); + Set disabledSet = + enabled ? Collections.emptySet() : Collections.singleton(changeId); + Object config = changeConfigClass.getConstructor(Set.class, Set.class) + .newInstance(enabledSet, disabledSet); + compatClass.getMethod("setOverrides", changeConfigClass).invoke(null, config); + } + + private static void clearCompatChangeOverrides() throws Exception { + try { + Class platformClass = + Class.forName(TestUtils.class.getPackage().getName() + ".Platform"); + platformClass.getMethod("setPqcDefaultEnabledForTesting", boolean.class) + .invoke(null, true); + return; + } catch (ReflectiveOperationException ignored) { + // Platform module uses android.compat.Compatibility overrides instead. + } + try { + Class compatClass = Class.forName("android.compat.Compatibility"); + compatClass.getMethod("clearOverrides").invoke(null); + } catch (Throwable ignored) { + // Ignore if no overrides were set. + } + } + + private static boolean isConscryptPqcCompatConfigPresent() { + try { + java.io.File apexDir = new java.io.File("/apex/com.android.conscrypt"); + if (apexDir.isDirectory()) { + return new java.io.File( + apexDir, "etc/compatconfig/conscrypt-platform-compat-config.xml") + .exists(); + } + } catch (SecurityException ignored) { + return false; + } + return true; + } + + private static boolean isTlsPqcFlagEnabled() { + String basePkg = TestUtils.class.getPackage().getName(); + try { + Class testFlagsClass = Class.forName(basePkg + ".test.flags.Flags"); + return (Boolean) testFlagsClass.getMethod("tlsPqcDefaultNamedGroups").invoke(null); + } catch (ClassNotFoundException ignored) { + // Fall back to non-repackaged Flags class when running outside conscrypt-tests. + } catch (ReflectiveOperationException e) { + return false; + } + try { + Class flagsClass = Class.forName(basePkg + ".flags.Flags"); + return (Boolean) flagsClass.getMethod("tlsPqcDefaultNamedGroups").invoke(null); + } catch (ClassNotFoundException e) { + return true; + } catch (ReflectiveOperationException e) { + return false; + } + } + + @Test + public void defaultNamedGroups_compatChangeEnabled_includesPqcAndExpectedGroups() + throws Exception { + assumeTrue(isConscryptPqcCompatConfigPresent()); + assumeTrue(isTlsPqcFlagEnabled()); + System.clearProperty("jdk.tls.namedGroups"); + setCompatChangeEnabled(ENABLE_TLS_PQC_DEFAULT_GROUPS, true); + TestSSLContext context = TestSSLContext.create(); + try { + ClientHello clientHello = TlsTester.captureTlsHandshakeClientHello( + executor, context.clientContext.getSocketFactory()); + EllipticCurvesHelloExtension groupsExtension = + (EllipticCurvesHelloExtension) clientHello.findExtensionByType( + HelloExtension.TYPE_ELLIPTIC_CURVES); + assertNotNull(groupsExtension); + assertEquals(Arrays.asList(EllipticCurve.X25519MLKEM768, EllipticCurve.X25519, + EllipticCurve.SECP256R1, EllipticCurve.SECP384R1), + groupsExtension.supported); + + final SSLSocket client = + (SSLSocket) context.clientContext.getSocketFactory().createSocket( + context.host, context.port); + final SSLSocket server = (SSLSocket) context.serverSocket.accept(); + Future s = runAsync(() -> { + server.startHandshake(); + return null; + }); + Future c = runAsync(() -> { + client.startHandshake(); + return null; + }); + s.get(); + c.get(); + assertEquals("X25519MLKEM768", getCurveName(client)); + assertEquals("X25519MLKEM768", getCurveName(server)); + client.close(); + server.close(); + } finally { + context.close(); + clearCompatChangeOverrides(); + } + } + + @Test + public void defaultNamedGroups_compatChangeDisabled_usesClassicalGroupsOnly() + throws Exception { + assumeTrue(isConscryptPqcCompatConfigPresent()); + System.clearProperty("jdk.tls.namedGroups"); + setCompatChangeEnabled(ENABLE_TLS_PQC_DEFAULT_GROUPS, false); + TestSSLContext context = TestSSLContext.create(); + try { + ClientHello clientHello = TlsTester.captureTlsHandshakeClientHello( + executor, context.clientContext.getSocketFactory()); + EllipticCurvesHelloExtension groupsExtension = + (EllipticCurvesHelloExtension) clientHello.findExtensionByType( + HelloExtension.TYPE_ELLIPTIC_CURVES); + assertNotNull(groupsExtension); + assertEquals(Arrays.asList(EllipticCurve.X25519, EllipticCurve.SECP256R1, + EllipticCurve.SECP384R1), + groupsExtension.supported); + + final SSLSocket client = + (SSLSocket) context.clientContext.getSocketFactory().createSocket( + context.host, context.port); + final SSLSocket server = (SSLSocket) context.serverSocket.accept(); + Future s = runAsync(() -> { + server.startHandshake(); + return null; + }); + Future c = runAsync(() -> { + client.startHandshake(); + return null; + }); + s.get(); + c.get(); + assertEquals("X25519", getCurveName(client)); + assertEquals("X25519", getCurveName(server)); + client.close(); + server.close(); + } finally { + context.close(); + clearCompatChangeOverrides(); + } + } + @Test public void handshake_p256IsSupportedByDefault() throws Exception { TestSSLContext context = TestSSLContext.create(); @@ -1374,64 +1480,6 @@ public void handshake_onlyMLKEM1024_works() throws Exception { context.close(); } - @Test - public void socket_setNamedGroups_works() throws Exception { - TestSSLContext context = TestSSLContext.create(); - final SSLSocket client = (SSLSocket) context.clientContext.getSocketFactory().createSocket( - context.host, context.port); - Conscrypt.setNamedGroups(client, new String[] {"P-384"}); - - // For the server, we don't set the named groups. P-384 should be - // enabled by default. - final SSLSocket server = (SSLSocket) context.serverSocket.accept(); - Future s = runAsync(() -> { - server.startHandshake(); - return null; - }); - Future c = runAsync(() -> { - client.startHandshake(); - return null; - }); - s.get(); - c.get(); - // This must works even if sslParametersSupportsNamedGroups is false. - assertEquals("P-384", getCurveName(client)); - assertEquals("P-384", getCurveName(server)); - client.close(); - server.close(); - context.close(); - } - - @Test - public void socketFactory_setNamedGroups_works() throws Exception { - TestSSLContext context = TestSSLContext.create(); - SSLSocketFactory clientSocketFactory = context.clientContext.getSocketFactory(); - Conscrypt.setNamedGroups(clientSocketFactory, new String[] {"P-384"}); - - final SSLSocket client = (SSLSocket) context.clientContext.getSocketFactory().createSocket( - context.host, context.port); - - // For the server, we don't set the named groups. P-384 should be - // enabled by default. - final SSLSocket server = (SSLSocket) context.serverSocket.accept(); - Future s = runAsync(() -> { - server.startHandshake(); - return null; - }); - Future c = runAsync(() -> { - client.startHandshake(); - return null; - }); - s.get(); - c.get(); - // This must works even if sslParametersSupportsNamedGroups is false. - assertEquals("P-384", getCurveName(client)); - assertEquals("P-384", getCurveName(server)); - client.close(); - server.close(); - context.close(); - } - @Test public void handshake_namedGroupsDontIntersect_throwsException() throws Exception { TestSSLContext context = TestSSLContext.create(); @@ -1528,6 +1576,65 @@ public void setNamedGroupsProperty_invalidValue_isIgnored() throws Exception { } } + @Test + public void + setNamedGroupsProperty_onlyUnrecognizedValuesWithNoPriorCache_fallsBackToDefaultGroups() + throws Exception { + // Clear the property and run a handshake to ensure any cached namedGroups property is reset + // to null. + System.clearProperty("jdk.tls.namedGroups"); + { + TestSSLContext context = TestSSLContext.create(); + final SSLSocket client = + (SSLSocket) context.clientContext.getSocketFactory().createSocket(context.host, + context.port); + final SSLSocket server = (SSLSocket) context.serverSocket.accept(); + Future s = runAsync(() -> { + server.startHandshake(); + return null; + }); + Future c = runAsync(() -> { + client.startHandshake(); + return null; + }); + s.get(); + c.get(); + client.close(); + server.close(); + context.close(); + } + + // Now set the property to only unrecognized group names when no valid cached property + // exists. + System.setProperty("jdk.tls.namedGroups", "invalid,invalid2"); + { + TestSSLContext context = TestSSLContext.create(); + final SSLSocket client = + (SSLSocket) context.clientContext.getSocketFactory().createSocket(context.host, + context.port); + final SSLSocket server = (SSLSocket) context.serverSocket.accept(); + Future s = runAsync(() -> { + server.startHandshake(); + return null; + }); + Future c = runAsync(() -> { + client.startHandshake(); + return null; + }); + s.get(); + c.get(); + String clientCurve = getCurveName(client); + String serverCurve = getCurveName(server); + assertTrue("Unexpected client curve: " + clientCurve, + clientCurve.equals("X25519") || clientCurve.equals("X25519MLKEM768")); + assertTrue("Unexpected server curve: " + serverCurve, + serverCurve.equals("X25519") || serverCurve.equals("X25519MLKEM768")); + client.close(); + server.close(); + context.close(); + } + } + @Test public void test_SSLSocket_ClientHello_supportedCurves() throws Exception { ForEachRunner.runNamed(sslSocketFactory -> { diff --git a/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSocketVersionCompatibilityTest.java b/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSocketVersionCompatibilityTest.java index 6fae78e61..85ef7fa8e 100644 --- a/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSocketVersionCompatibilityTest.java +++ b/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSocketVersionCompatibilityTest.java @@ -89,6 +89,7 @@ import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -1546,41 +1547,6 @@ public void test_SSLSocket_interrupt_write_withAutoclose() throws Exception { server.close(); } - @Test - public void test_SSLSocket_ClientHello_record_size() throws Exception { - // This test checks the size of ClientHello of the default SSLSocket. TLS/SSL handshakes - // with older/unpatched F5/BIG-IP appliances are known to stall and time out when - // the fragment containing ClientHello is between 256 and 511 (inclusive) bytes long. - SSLContext sslContext = SSLContext.getInstance(clientVersion); - sslContext.init(null, null, null); - SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); - sslSocketFactory = new DelegatingSSLSocketFactory(sslSocketFactory) { - @Override - protected SSLSocket configureSocket(SSLSocket socket) { - // Enable SNI extension on the socket (this is typically enabled by default) - // to increase the size of ClientHello. - setHostname(socket); - - // Enable Session Tickets extension on the socket (this is typically enabled - // by default) to increase the size of ClientHello. - enableSessionTickets(socket); - return socket; - } - }; - TlsRecord firstReceivedTlsRecord = - TlsTester.captureTlsHandshakeFirstTlsRecord(executor, sslSocketFactory); - assertEquals("TLS record type", TlsProtocols.HANDSHAKE, firstReceivedTlsRecord.type); - HandshakeMessage handshakeMessage = HandshakeMessage.read( - new DataInputStream(new ByteArrayInputStream(firstReceivedTlsRecord.fragment))); - assertEquals("HandshakeMessage type", HandshakeMessage.TYPE_CLIENT_HELLO, - handshakeMessage.type); - int fragmentLength = firstReceivedTlsRecord.fragment.length; - if ((fragmentLength >= 256) && (fragmentLength <= 511)) { - fail("Fragment containing ClientHello is of dangerous length: " + fragmentLength - + " bytes"); - } - } - @Test public void test_SSLSocket_ClientHello_SNI() throws Exception { ForEachRunner.runNamed(sslSocketFactory -> { @@ -1886,6 +1852,8 @@ public void test_SSLSocket_TlsUnique() throws Exception { } finally { pair.close(); } + assertNull(Conscrypt.getTlsUnique(pair.client)); + assertNull(Conscrypt.getTlsUnique(pair.server)); } // Tests that all cipher suites have a 12-byte tls-unique channel binding value. If this @@ -2003,6 +1971,50 @@ public void test_SSLSocket_EKM() throws Exception { } finally { pair.close(); } + assertNull(Conscrypt.exportKeyingMaterial(pair.client, "FOO", null, 20)); + assertNull(Conscrypt.exportKeyingMaterial(pair.server, "FOO", null, 20)); + } + + @Test + public void test_SSLSocket_EKM_concurrentClose() throws Exception { + for (int iter = 0; iter < 50; iter++) { + TestSSLContext context = new TestSSLContext.Builder() + .clientProtocol(clientVersion) + .serverProtocol(serverVersion) + .build(); + TestSSLSocketPair pair = TestSSLSocketPair.create(context); + pair.connect(); + + AtomicReference error = new AtomicReference<>(); + Thread getterThread = new Thread(() -> { + try { + for (int k = 0; k < 500; k++) { + Conscrypt.exportKeyingMaterial(pair.client, "FOO", null, 20); + Conscrypt.getTlsUnique(pair.client); + } + } catch (Throwable t) { + error.set(t); + } + }); + + Thread closerThread = new Thread(() -> { + try { + pair.close(); + } catch (Throwable t) { + error.set(t); + } + }); + + getterThread.start(); + closerThread.start(); + getterThread.join(); + closerThread.join(); + + if (error.get() != null) { + throw new AssertionError("Encountered exception in concurrent EKM/close", + error.get()); + } + } } // Tests that a socket will close cleanly even if it fails to create due to an diff --git a/common/src/test/java/org/conscrypt/javax/net/ssl/TrustManagerFactoryTest.java b/common/src/test/java/org/conscrypt/javax/net/ssl/TrustManagerFactoryTest.java index 136d43e14..1b7486cc3 100644 --- a/common/src/test/java/org/conscrypt/javax/net/ssl/TrustManagerFactoryTest.java +++ b/common/src/test/java/org/conscrypt/javax/net/ssl/TrustManagerFactoryTest.java @@ -24,8 +24,7 @@ import static org.junit.Assert.fail; import org.bouncycastle.asn1.x509.KeyPurposeId; -import org.conscrypt.Conscrypt; -import org.conscrypt.Spake2PlusTrustManager; +import org.conscrypt.TestUtils; import org.conscrypt.java.security.StandardNames; import org.conscrypt.java.security.TestKeyStore; import org.junit.Test; @@ -69,7 +68,7 @@ private static TestKeyStore getTestKeyStore() throws Exception { private static boolean supportsManagerFactoryParameters(TrustManagerFactory tmf) { return (StandardNames.IS_RI && tmf.getAlgorithm().equals("PKIX") - && !Conscrypt.isConscrypt(tmf.getProvider())); + && !TestUtils.isConscrypt(tmf.getProvider())); } @Test @@ -163,17 +162,9 @@ private void test_TrustManagerFactory_getTrustManagers(TrustManagerFactory tmf) if (trustManager instanceof X509TrustManager) { test_X509TrustManager(tmf.getProvider(), (X509TrustManager) trustManager); } - if (trustManager instanceof Spake2PlusTrustManager) { - test_pakeTrustManager((Spake2PlusTrustManager) trustManager); - } } } - private void test_pakeTrustManager(Spake2PlusTrustManager tm) throws Exception { - tm.checkClientTrusted(); - tm.checkServerTrusted(); - } - private void test_X509TrustManager(Provider p, X509TrustManager tm) throws Exception { for (String keyType : KEY_TYPES) { X509Certificate[] issuers = tm.getAcceptedIssuers(); @@ -183,7 +174,7 @@ private void test_X509TrustManager(Provider p, X509TrustManager tm) throws Excep boolean defaultTrustManager // RI de-duplicates certs from TrustedCertificateEntry and PrivateKeyEntry = issuers.length - > (StandardNames.IS_RI && !Conscrypt.isConscrypt(p) ? 1 : 2) * KEY_TYPES.length; + > (StandardNames.IS_RI && !TestUtils.isConscrypt(p) ? 1 : 2) * KEY_TYPES.length; String keyAlgName = TestKeyStore.keyAlgorithm(keyType); String sigAlgName = TestKeyStore.signatureAlgorithm(keyType); diff --git a/common/src/test/java/org/conscrypt/util/DerUtilsTest.java b/common/src/test/java/org/conscrypt/util/DerUtilsTest.java new file mode 100644 index 000000000..7aeef0868 --- /dev/null +++ b/common/src/test/java/org/conscrypt/util/DerUtilsTest.java @@ -0,0 +1,230 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * 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 org.conscrypt.util; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.math.BigInteger; +import java.nio.ByteBuffer; + +@RunWith(JUnit4.class) +public class DerUtilsTest { + @Test + public void testReadOctetString_shortForm() throws Exception { + byte[] input = new byte[] {0x04, 0x04, 0x01, 0x02, 0x03, 0x04}; + byte[] expected = new byte[] {0x01, 0x02, 0x03, 0x04}; + + assertArrayEquals(expected, DerUtils.readOctetString(input)); + assertArrayEquals(expected, DerUtils.readOctetString(ByteBuffer.wrap(input))); + } + + @Test + public void testReadOctetString_longForm() throws Exception { + byte[] payload = new byte[200]; + payload[0] = 0x42; + payload[199] = 0x24; + + byte[] input = new byte[203]; + input[0] = 0x04; // TAG_OCTET_STRING + input[1] = (byte) 0x81; // long form, 1 length byte + input[2] = (byte) 200; // length + System.arraycopy(payload, 0, input, 3, 200); + + assertArrayEquals(payload, DerUtils.readOctetString(input)); + assertArrayEquals(payload, DerUtils.readOctetString(ByteBuffer.wrap(input))); + } + + @Test + public void testReadOctetString_invalidTag() { + byte[] input = new byte[] {0x30, 0x02, 0x01, 0x02}; // SEQUENCE tag instead of OCTET STRING + assertThrows(SerializationException.class, () -> DerUtils.readOctetString(input)); + assertThrows(SerializationException.class, + () -> DerUtils.readOctetString(ByteBuffer.wrap(input))); + + // Context-specific [4] IMPLICIT (0x84) has low 6 bits 0x04, but must be rejected + byte[] contextTagInput = new byte[] {(byte) 0x84, 0x02, 0x01, 0x02}; + assertThrows(SerializationException.class, () -> DerUtils.readOctetString(contextTagInput)); + assertThrows(SerializationException.class, + () -> DerUtils.readOctetString(ByteBuffer.wrap(contextTagInput))); + } + + @Test + public void testReadOctetString_truncatedPayload() { + byte[] input = new byte[] {0x04, 0x05, 0x01, 0x02}; // length claims 5 bytes, only 2 present + assertThrows(SerializationException.class, () -> DerUtils.readOctetString(input)); + assertThrows(SerializationException.class, + () -> DerUtils.readOctetString(ByteBuffer.wrap(input))); + } + + @Test + public void testReadOctetString_nonMinimalLength() { + // Long form length 0x81 0x04 for length 4, which could fit in short form + byte[] input = new byte[] {0x04, (byte) 0x81, 0x04, 0x01, 0x02, 0x03, 0x04}; + assertThrows(SerializationException.class, () -> DerUtils.readOctetString(input)); + assertThrows(SerializationException.class, + () -> DerUtils.readOctetString(ByteBuffer.wrap(input))); + } + + @Test + public void testReadOctetString_nullInput() { + assertThrows(SerializationException.class, () -> DerUtils.readOctetString((byte[]) null)); + } + + @Test + public void testReadOctetString_trailingBytes() { + byte[] input = new byte[] {0x04, 0x02, 0x01, 0x02, (byte) 0xFF}; + assertThrows(SerializationException.class, () -> DerUtils.readOctetString(input)); + } + + @Test + public void testReadSequenceElement() throws Exception { + byte[] raw = new byte[] {0x30, 0x03, 0x02, 0x01, + 0x05, 0x00}; // SEQUENCE containing INTEGER 5, + trailing byte + ByteBuffer buf = ByteBuffer.wrap(raw); + byte[] element = DerUtils.readSequenceElement(buf); + + assertArrayEquals(new byte[] {0x30, 0x03, 0x02, 0x01, 0x05}, element); + assertEquals(1, buf.remaining()); + } + + @Test + public void testReadSequenceElement_wrongTag() { + ByteBuffer buf = ByteBuffer.wrap(new byte[] {0x02, 0x01, 0x05}); + assertThrows(SerializationException.class, () -> DerUtils.readSequenceElement(buf)); + assertEquals(0, buf.position()); + } + + @Test + public void testSkipIntegerAndSequence() throws Exception { + byte[] raw = new byte[] {0x02, 0x01, 0x05, 0x30, 0x01, 0x00, 0x04}; + ByteBuffer buf = ByteBuffer.wrap(raw); + DerUtils.skipInteger(buf); + assertEquals(4, buf.remaining()); + DerUtils.skipSequence(buf); + assertEquals(1, buf.remaining()); + assertEquals(0x04, buf.get()); + + assertThrows(SerializationException.class, + () -> DerUtils.skipSequence(ByteBuffer.wrap(raw))); + assertThrows(SerializationException.class, + () -> DerUtils.skipInteger(ByteBuffer.wrap(new byte[] {0x30, 0x00}))); + } + + @Test + public void testReadSequence() throws Exception { + byte[] raw = new byte[] {0x30, 0x03, 0x02, 0x01, 0x05, (byte) 0xFF}; + ByteBuffer buf = ByteBuffer.wrap(raw); + ByteBuffer seq = DerUtils.readSequence(buf); + + assertEquals(1, buf.remaining()); + assertEquals((byte) 0xFF, buf.get()); + assertEquals(3, seq.remaining()); + assertEquals(0x02, seq.get()); + ByteBuffer wrongTagBuf = ByteBuffer.wrap(new byte[] {0x02, 0x01, 0x05}); + assertThrows(SerializationException.class, () -> DerUtils.readSequence(wrongTagBuf)); + assertEquals(0, wrongTagBuf.position()); + } + + @Test + public void testReadOptionalElement() throws Exception { + byte[] withVersion = new byte[] {(byte) 0xA0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x01, 0x01}; + ByteBuffer buf1 = ByteBuffer.wrap(withVersion); + byte[] version = DerUtils.readOptionalElement(buf1, DerUtils.TAG_CONTEXT_EXPLICIT_0); + assertArrayEquals(new byte[] {(byte) 0xA0, 0x03, 0x02, 0x01, 0x02}, version); + assertEquals(3, buf1.remaining()); + + byte[] withoutVersion = new byte[] {0x02, 0x01, 0x01}; + ByteBuffer buf2 = ByteBuffer.wrap(withoutVersion); + assertEquals(null, DerUtils.readOptionalElement(buf2, DerUtils.TAG_CONTEXT_EXPLICIT_0)); + assertEquals(3, buf2.remaining()); + } + + @Test + public void testReadBigInteger() throws Exception { + // Positive integer 0x01000000000000 (2^48) + byte[] valid = new byte[] {0x02, 0x07, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + assertEquals(BigInteger.ONE.shiftLeft(48), DerUtils.readBigInteger(ByteBuffer.wrap(valid))); + + // Positive integer requiring leading 0x00 sign byte (0x80 = 128) + byte[] leadingZero = new byte[] {0x02, 0x02, 0x00, (byte) 0x80}; + assertEquals(BigInteger.valueOf(128), + DerUtils.readBigInteger(ByteBuffer.wrap(leadingZero))); + + // Non-minimal leading 0x00 (next byte < 0x80) must be rejected + byte[] nonMinimalZero = new byte[] {0x02, 0x02, 0x00, 0x01}; + assertThrows(SerializationException.class, + () -> DerUtils.readBigInteger(ByteBuffer.wrap(nonMinimalZero))); + + // Non-minimal leading 0xFF (next byte >= 0x80) must be rejected + byte[] nonMinimalNeg = new byte[] {0x02, 0x02, (byte) 0xFF, (byte) 0x80}; + assertThrows(SerializationException.class, + () -> DerUtils.readBigInteger(ByteBuffer.wrap(nonMinimalNeg))); + + // Empty INTEGER payload must be rejected + byte[] empty = new byte[] {0x02, 0x00}; + assertThrows(SerializationException.class, + () -> DerUtils.readBigInteger(ByteBuffer.wrap(empty))); + } + + @Test + public void testReadRelativeOidBytesAndFormatRelativeOid() throws Exception { + // 32473.1 -> 81 fd 59 01 + byte[] valid = new byte[] {0x0D, 0x04, (byte) 0x81, (byte) 0xFD, 0x59, 0x01}; + byte[] rawBytes = DerUtils.readRelativeOidBytes(ByteBuffer.wrap(valid)); + assertArrayEquals(new byte[] {(byte) 0x81, (byte) 0xFD, 0x59, 0x01}, rawBytes); + assertEquals("32473.1", DerUtils.formatRelativeOid(rawBytes)); + + // Non-minimal sub-identifier (leading 0x80) must be rejected + byte[] nonMinimal = new byte[] {0x0D, 0x02, (byte) 0x80, 0x01}; + assertThrows(SerializationException.class, + () -> DerUtils.readRelativeOidBytes(ByteBuffer.wrap(nonMinimal))); + + // Truncated sub-identifier (continuation bit set on final byte) must be rejected + byte[] truncated = new byte[] {0x0D, 0x01, (byte) 0x81}; + assertThrows(SerializationException.class, + () -> DerUtils.readRelativeOidBytes(ByteBuffer.wrap(truncated))); + + // Empty RELATIVE-OID payload must be rejected + assertThrows(SerializationException.class, + () -> DerUtils.readRelativeOidBytes(ByteBuffer.wrap(new byte[] {0x0D, 0x00}))); + } + + @Test + public void testReadSetAndReadOid() throws Exception { + // SET containing OID 1.3.6.1.4.1.44363.47.3 (06 0A 2B 06 01 04 01 82 DA 4B 2F 03) + byte[] setWithOid = new byte[] {0x31, 0x0C, 0x06, 0x0A, 0x2B, 0x06, 0x01, + 0x04, 0x01, (byte) 0x82, (byte) 0xDA, 0x4B, 0x2F, 0x03}; + ByteBuffer setBuf = DerUtils.readSet(ByteBuffer.wrap(setWithOid)); + assertEquals("1.3.6.1.4.1.44363.47.3", DerUtils.readOid(setBuf)); + assertEquals(0, setBuf.remaining()); + + // OID under joint-iso-itu-t (2.16.840.1.101.3.4.3.17 = ML-DSA-44) + byte[] mlDsa44Oid = new byte[] {0x06, 0x09, 0x60, (byte) 0x86, 0x48, 0x01, + 0x65, 0x03, 0x04, 0x03, 0x11}; + assertEquals("2.16.840.1.101.3.4.3.17", DerUtils.readOid(ByteBuffer.wrap(mlDsa44Oid))); + + // Empty OID payload must be rejected + assertThrows(SerializationException.class, + () -> DerUtils.readOid(ByteBuffer.wrap(new byte[] {0x06, 0x00}))); + } +} diff --git a/common/src/test/java/org/conscrypt/util/MtcUtilsTest.java b/common/src/test/java/org/conscrypt/util/MtcUtilsTest.java new file mode 100644 index 000000000..237056ba0 --- /dev/null +++ b/common/src/test/java/org/conscrypt/util/MtcUtilsTest.java @@ -0,0 +1,193 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * 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 org.conscrypt.util; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.ByteArrayInputStream; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; + +@RunWith(JUnit4.class) +public class MtcUtilsTest { + // Valid MTC CA certificate (CA ID 32473.1, Subject RDN 1.3.6.1.4.1.44363.47.3=#0D0481FD5901, + // critical extension 1.3.6.1.4.1.44363.47.4 with minSerial=0x1000000000000, + // maxSerial=0x5ffffffffffff). + private static final String VALID_MTC_CA_PEM = "-----BEGIN CERTIFICATE-----\n" + + "MIIGADCCBe2gAwIBAgIBATAKBggrBgEFBQcGJDAQMQ4wDAYIKwYBBQUHGQEMADAe\n" + + "Fw0yMDAxMDEwMDAwMDBaFw0zMDEyMzEyMzU5NTlaMBYxFDASBgorBgEEAYLaSy8D\n" + + "DQSB/VkBMIIFMjALBglghkgBZQMEAxEDggUhAFzN/zgGMsaBJd+nNwxE0mZlxiSI\n" + + "OIrZwLSScveu9rIIIPfYjTEvKetlhX1W5quTKhFrZqvMzskfSBqUqMuqMCcykhiC\n" + + "5x5r4GVBvK2GCaP2sTCOM5kJ9hzwCevraUeClJhgDB8dRpYuJox68eNkNnfDsYTN\n" + + "pfB1HUey8jfDhI9+tGmtHC7apuIEyeYyAMwYobBeCquk4ikCb04KoYQRWkhPSDnN\n" + + "4XLfY+otOwXy4Ev96V6pK74UrTwHKSzMQPMTF0O8zsVJKgSSNfd1zJp+Tk/CHZDD\n" + + "2+ZcFMzcU2BdAO0QiMb6O3BOLK5eg1ZFcX/abzQyNcBo3hfuvHlSTsOezgh3u4/x\n" + + "q4zALLfVTcNQAqY0E6HTSrt+3K7MJhmRwQfr8IsyB+x8nAuesYGrzJbCzDq4Wmfu\n" + + "R+LCOujNQNT26yBwTIbEUCCRQAz6htYICqmW8jD6hjLnlizUE1lTG2kp/lvtqJj8\n" + + "AGu654MkUsxVOUAMP1LV3DTHq5RmhZd5oET63yQ/pj+CBzsZx9iJf3ovbNJ7LRGr\n" + + "nMba9L85KTIjF9vtJKdiQp5Td7gyUlDhKeajvk4VJ6baQa1xuYxa6thcBt18E8Jx\n" + + "OBvXO8qw+l3jOJnzZdDtnFg3TajfCb1OUZ5cpcEajPtT7IhgbGhKu1B8bcIynUy8\n" + + "N6Wb99SBdBn8xbaWJOdsq++Yi45VVBdlCL/4psXG6hRoot4V21yUO9xQCQCXYsSI\n" + + "EQwnUmOkoK82UQ3v0n5Z5K/KZhS6bIWc7zEAp5WU7DBAye8V+rSAoLvxeJE9uvMU\n" + + "2twQDBVOQAfqVSpskzMZpypMdPPjnBUk0vUaOIih2UuCqLooT/yL54PXO3WHLPj0\n" + + "G6XKmQ92ksFPyc7skwwnYSh2PDk879/m6i4/8s37i9e6T68O5oQWf7CMShV2z50w\n" + + "Dkuy34hzHQzCtJ0HWdH73q31etKfIvR11ni7aWVmRGlmU8xXjMATrgJ3w9I8aVY4\n" + + "M9LG0ykCJYhwZG74zr4WRTr/LEdr97H06D7TZKsBM/jdfHYKLu3VYciHAnwI7iAM\n" + + "htR3cw9lMGs1NLKdYi12vWdSIiSH//1o6Foa+Yej9j+h4LbNn0orlEZMLBPx+bAf\n" + + "OzYKIYegyAsxguFvDYDCIhOQ6ZuCD49RyXOKgcfgklFi2d3wiQSyPMAl8nGG1Hun\n" + + "PdJWVRx4tPx+8/SY8TVcFiTciW/Itdy8uytslOR0h5fB5dQFWxJXaClRzlvOqHXA\n" + + "bSthBETzCpMDkeRk4qg5MCmrTFepTeshtKC3cwRcPXQo0jys3VrVASLjQehcm7k1\n" + + "hFXo9TkfBY5hYShjOYoekIuy3wI3dr+SeARSJre2VMxdyd6ZEtQrfaz0pbQaOIr5\n" + + "CqcM7uyR/lycHlbnwsE2pKPubbFXev836BjfQItaIX2SExK/1rlbXs/rDAidw3Ts\n" + + "c2XO2Lh8AfG5SPgcJeo3cOQhjhywqECjwcPsTl0CwHDqiW++8wyBGPTyl5niOYPu\n" + + "VQhAF6wPYUQD8xBywy5QpgvRajWv7LZ0/wztQhkjSiAs453WwRn2WaDlXqAX3xjC\n" + + "p7UC5NCoB02LCE1xzQa++plEg5uvdMPyFeGRxqmBmU0jUH4PeJyEQJXj0lj2/W+y\n" + + "YyjbwO51qHyK6AuLyDexkLR2y/H2y+cG4LfRYpI4kmTTItPgzEqSbuhhEKIPZRGR\n" + + "y5p/8IJvwTDmjQ3H9FwJH9yKBxIoDJIB+9wYkumo/GbvyZ2m1lAVsxI6WqajVzBV\n" + + "MA4GA1UdDwEB/wQEAwICBDAPBgNVHRMBAf8EBTADAQH/MDIGCisGAQQBgtpLLwQB\n" + + "Af8EITAfMAsGCWCGSAFlAwQDEQIHAQAAAAAAAAIHBf///////zAKBggrBgEFBQcG\n" + + "JAMBAA==\n" + + "-----END CERTIFICATE-----\n"; + + private static X509Certificate parseCert(String pem) throws Exception { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate( + new ByteArrayInputStream(pem.getBytes(StandardCharsets.UTF_8))); + } + + @Test + public void testIsMtcCaCertificateAndExtractCaId() throws Exception { + X509Certificate caCert = parseCert(VALID_MTC_CA_PEM); + assertTrue(MtcUtils.isMtcCaCertificate(caCert)); + assertFalse(MtcUtils.hasMtcSignatureAlgorithm(caCert)); + + TrustAnchor anchor = new TrustAnchor(caCert, null); + byte[] expectedCaId = new byte[] {(byte) 0x81, (byte) 0xFD, 0x59, 0x01}; + assertArrayEquals(expectedCaId, MtcUtils.extractCaId(anchor)); + assertEquals("2.16.840.1.101.3.4.3.17", MtcUtils.extractCaSigAlg(anchor)); + + byte[] expectedLogId0 = new byte[] {(byte) 0x81, (byte) 0xFD, 0x59, 0x01, 0x00, 0x00}; + assertArrayEquals(expectedLogId0, MtcUtils.getLogId(expectedCaId, 0)); + assertEquals("32473.1.0.0", DerUtils.formatRelativeOid(expectedLogId0)); + + byte[] expectedLogId128 = + new byte[] {(byte) 0x81, (byte) 0xFD, 0x59, 0x01, 0x00, (byte) 0x81, 0x00}; + assertArrayEquals(expectedLogId128, MtcUtils.getLogId(expectedCaId, 128)); + assertEquals("32473.1.0.128", DerUtils.formatRelativeOid(expectedLogId128)); + } + + @Test + public void testValidateSerialInCaRange() throws Exception { + X509Certificate caCert = parseCert(VALID_MTC_CA_PEM); + TrustAnchor anchor = new TrustAnchor(caCert, null); + + // Exact minSerial (0x1000000000000) and maxSerial (0x5ffffffffffff) should succeed + BigInteger minSerial = BigInteger.ONE.shiftLeft(48); + BigInteger maxSerial = BigInteger.valueOf(6).shiftLeft(48).subtract(BigInteger.ONE); + MtcUtils.validateSerialInCaRange(anchor, minSerial); + MtcUtils.validateSerialInCaRange(anchor, maxSerial); + + // Below minSerial (0x1000000000000 - 1) and above maxSerial (0x6000000000000) must fail + BigInteger belowMin = minSerial.subtract(BigInteger.ONE); + BigInteger aboveMax = maxSerial.add(BigInteger.ONE); + assertThrows(CertificateException.class, + () -> MtcUtils.validateSerialInCaRange(anchor, belowMin)); + assertThrows(CertificateException.class, + () -> MtcUtils.validateSerialInCaRange(anchor, aboveMax)); + } + + @Test + public void testFindCaSignature_shortlexOrderingAndUniqueness() throws Exception { + // cosigner_id for 32473.1 is 4 bytes: 81 fd 59 01 + byte[] cid1 = new byte[] {(byte) 0x81, (byte) 0xFD, 0x59, 0x01}; + // cosigner_id for 32473.2 is 4 bytes: 81 fd 59 02 + byte[] cid2 = new byte[] {(byte) 0x81, (byte) 0xFD, 0x59, 0x02}; + // cosigner_id for 32473.2.3 is 5 bytes: 81 fd 59 02 03 + byte[] cid3 = new byte[] {(byte) 0x81, (byte) 0xFD, 0x59, 0x02, 0x03}; + + byte[] sig1 = new byte[] {0x11, 0x22}; + byte[] sig2 = new byte[] {0x33, 0x44}; + byte[] sig3 = new byte[] {0x55, 0x66}; + + // Valid shortlex order: cid1 (4B) < cid2 (4B) < cid3 (5B) + byte[] validSigs = + packSignatures(new byte[][] {cid1, cid2, cid3}, new byte[][] {sig1, sig2, sig3}); + assertArrayEquals(sig1, MtcUtils.findCaSignature(validSigs, cid1)); + assertArrayEquals(sig2, MtcUtils.findCaSignature(validSigs, cid2)); + + // Duplicate cosigner_id after matched entry must still be rejected + byte[] duplicateAfterMatch = + packSignatures(new byte[][] {cid1, cid2, cid2}, new byte[][] {sig1, sig2, sig3}); + assertThrows(CertificateException.class, + () -> MtcUtils.findCaSignature(duplicateAfterMatch, cid1)); + + // Out-of-order cosigner_id (5-byte cid3 before 4-byte cid2) must be rejected + byte[] outOfOrder = + packSignatures(new byte[][] {cid1, cid3, cid2}, new byte[][] {sig1, sig3, sig2}); + assertThrows(CertificateException.class, () -> MtcUtils.findCaSignature(outOfOrder, cid1)); + + // Empty cosigner_id must be rejected + byte[] emptyCid = packSignatures(new byte[][] {new byte[0]}, new byte[][] {sig1}); + assertThrows(CertificateException.class, () -> MtcUtils.findCaSignature(emptyCid, cid1)); + } + + @Test + public void testIsValidSubtree() { + // Valid full power-of-two subtrees + assertTrue(MtcUtils.isValidSubtree(0, 8)); + assertTrue(MtcUtils.isValidSubtree(8, 16)); + assertTrue(MtcUtils.isValidSubtree(0, 1L << 48)); + + // Valid empty subtree (start == end) and partial subtree (start % 2^n == 0) + assertTrue(MtcUtils.isValidSubtree(8, 8)); + assertTrue(MtcUtils.isValidSubtree(0, 10)); + + // Invalid bounds: start < 0, start > end, end > 2^48, or unaligned [2, 6) + assertFalse(MtcUtils.isValidSubtree(-1, 8)); + assertFalse(MtcUtils.isValidSubtree(8, 4)); + assertFalse(MtcUtils.isValidSubtree(0, (1L << 48) + 1)); + assertFalse(MtcUtils.isValidSubtree(2, 6)); + } + + private static byte[] packSignatures(byte[][] cids, byte[][] sigs) { + int total = 0; + for (int i = 0; i < cids.length; i++) { + total += 1 + cids[i].length + 2 + sigs[i].length; + } + ByteBuffer buf = ByteBuffer.allocate(total); + for (int i = 0; i < cids.length; i++) { + buf.put((byte) cids[i].length); + buf.put(cids[i]); + buf.putShort((short) sigs[i].length); + buf.put(sigs[i]); + } + return buf.array(); + } +} diff --git a/openjdk/src/main/java/org/conscrypt/Platform.java b/openjdk/src/main/java/org/conscrypt/Platform.java index 337d13df1..503e982a5 100644 --- a/openjdk/src/main/java/org/conscrypt/Platform.java +++ b/openjdk/src/main/java/org/conscrypt/Platform.java @@ -794,6 +794,21 @@ public static boolean isPakeSupported() { return false; } + public static boolean isPqcKemKdfSupported() { + return true; + } + + public static final long ENABLE_TLS_PQC_DEFAULT_GROUPS = 556729260L; + private static volatile boolean pqcDefaultEnabled = true; + + public static boolean isPqcDefaultEnabled() { + return pqcDefaultEnabled; + } + + public static void setPqcDefaultEnabledForTesting(boolean enabled) { + pqcDefaultEnabled = enabled; + } + public static boolean isSdkGreater(int sdk) { return false; } diff --git a/openjdk/src/test/java/org/conscrypt/ConscryptAndroidSuite.java b/openjdk/src/test/java/org/conscrypt/ConscryptAndroidSuite.java index ebe411a22..114986df3 100644 --- a/openjdk/src/test/java/org/conscrypt/ConscryptAndroidSuite.java +++ b/openjdk/src/test/java/org/conscrypt/ConscryptAndroidSuite.java @@ -35,12 +35,15 @@ import org.conscrypt.java.security.KeyFactoryTestDSA; import org.conscrypt.java.security.KeyFactoryTestEC; import org.conscrypt.java.security.KeyFactoryTestRSACrt; +import org.conscrypt.java.security.KeyFactoryTestRSACustom; +import org.conscrypt.java.security.KeyFactoryTestXDH; import org.conscrypt.java.security.KeyPairGeneratorTest; import org.conscrypt.java.security.KeyPairGeneratorTestDH; import org.conscrypt.java.security.KeyPairGeneratorTestDSA; import org.conscrypt.java.security.KeyPairGeneratorTestRSA; import org.conscrypt.java.security.KeyPairGeneratorTestXDH; import org.conscrypt.java.security.MessageDigestTest; +import org.conscrypt.java.security.SecureRandomTest; import org.conscrypt.java.security.SignatureTest; import org.conscrypt.java.security.cert.CertificateFactoryTest; import org.conscrypt.java.security.cert.X509CRLTest; @@ -63,12 +66,15 @@ import org.conscrypt.javax.net.ssl.SSLServerSocketTest; import org.conscrypt.javax.net.ssl.SSLSessionContextTest; import org.conscrypt.javax.net.ssl.SSLSessionTest; +import org.conscrypt.javax.net.ssl.SSLSocketImplTest; import org.conscrypt.javax.net.ssl.SSLSocketTest; import org.conscrypt.javax.net.ssl.SslEngineAndSocketResumptionTest; import org.conscrypt.javax.net.ssl.X509KeyManagerTest; import org.conscrypt.metrics.CipherSuiteTest; import org.conscrypt.metrics.OptionalMethodTest; import org.conscrypt.metrics.ProtocolTest; +import org.conscrypt.util.DerUtilsTest; +import org.conscrypt.util.MtcUtilsTest; import org.junit.BeforeClass; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -87,6 +93,7 @@ CertPinManagerTest.class, ChaCha20Poly1305Test.class, ChainStrengthAnalyzerTest.class, + CompositeMlDsaTest.class, DuckTypedHpkeSpiTest.class, EdDsaTest.class, ExposedByteArrayOutputStreamTest.class, @@ -98,17 +105,23 @@ HpkeContextSenderTest.class, HpkeSuiteTest.class, HpkeTestVectorsTest.class, + KeyManagerFactoryImplTest.class, KeySpecUtilTest.class, MlDsaTest.class, + MlKemTest.class, NativeCryptoArgTest.class, NativeCryptoTest.class, NativeSslTest.class, NativeRefTest.class, NativeSslSessionTest.class, + OpenSSLCipherTest.class, OpenSSLECGroupContextTest.class, OpenSSLKeyTest.class, OpenSSLX509CertificateTest.class, OpenSSLX509CRLTest.class, + OpenSslCompositeMlDsaKeyFactoryTest.class, + OpenSslCompositeMlDsaPrivateKeyTest.class, + OpenSslCompositeMlDsaPublicKeyTest.class, SignatureThreadMisuseTest.class, SSLUtilsTest.class, SlhDsaTest.class, @@ -119,6 +132,9 @@ // org.conscrypt.ct tests VerifierTest.class, SerializationTest.class, + // org.conscrypt.util tests + DerUtilsTest.class, + MtcUtilsTest.class, // java.security tests CertificateFactoryTest.class, X509CertificateTest.class, @@ -140,12 +156,15 @@ KeyFactoryTestDSA.class, KeyFactoryTestEC.class, KeyFactoryTestRSACrt.class, + KeyFactoryTestRSACustom.class, + KeyFactoryTestXDH.class, KeyPairGeneratorTest.class, KeyPairGeneratorTestDH.class, KeyPairGeneratorTestDSA.class, KeyPairGeneratorTestRSA.class, KeyPairGeneratorTestXDH.class, MessageDigestTest.class, + SecureRandomTest.class, SignatureTest.class, // javax.crypto tests AeadCipherTest.class, @@ -170,6 +189,7 @@ SSLServerSocketTest.class, SSLSessionContextTest.class, SSLSessionTest.class, + SSLSocketImplTest.class, SSLSocketTest.class, SslEngineAndSocketResumptionTest.class, VeryBasicHttpServerTest.class, diff --git a/openjdk/src/test/java/org/conscrypt/ConscryptAndroidWithoutTlsSuite.java b/openjdk/src/test/java/org/conscrypt/ConscryptAndroidWithoutTlsSuite.java index 950600859..b804a541e 100644 --- a/openjdk/src/test/java/org/conscrypt/ConscryptAndroidWithoutTlsSuite.java +++ b/openjdk/src/test/java/org/conscrypt/ConscryptAndroidWithoutTlsSuite.java @@ -35,12 +35,15 @@ import org.conscrypt.java.security.KeyFactoryTestDSA; import org.conscrypt.java.security.KeyFactoryTestEC; import org.conscrypt.java.security.KeyFactoryTestRSACrt; +import org.conscrypt.java.security.KeyFactoryTestRSACustom; +import org.conscrypt.java.security.KeyFactoryTestXDH; import org.conscrypt.java.security.KeyPairGeneratorTest; import org.conscrypt.java.security.KeyPairGeneratorTestDH; import org.conscrypt.java.security.KeyPairGeneratorTestDSA; import org.conscrypt.java.security.KeyPairGeneratorTestRSA; import org.conscrypt.java.security.KeyPairGeneratorTestXDH; import org.conscrypt.java.security.MessageDigestTest; +import org.conscrypt.java.security.SecureRandomTest; import org.conscrypt.java.security.SignatureTest; import org.conscrypt.java.security.cert.CertificateFactoryTest; import org.conscrypt.java.security.cert.X509CRLTest; @@ -61,6 +64,8 @@ import org.conscrypt.metrics.CipherSuiteTest; import org.conscrypt.metrics.OptionalMethodTest; import org.conscrypt.metrics.ProtocolTest; +import org.conscrypt.util.DerUtilsTest; +import org.conscrypt.util.MtcUtilsTest; import org.junit.BeforeClass; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -79,6 +84,7 @@ CertPinManagerTest.class, ChaCha20Poly1305Test.class, ChainStrengthAnalyzerTest.class, + CompositeMlDsaTest.class, DuckTypedHpkeSpiTest.class, EdDsaTest.class, ExposedByteArrayOutputStreamTest.class, @@ -90,17 +96,23 @@ HpkeContextSenderTest.class, HpkeSuiteTest.class, HpkeTestVectorsTest.class, + KeyManagerFactoryImplTest.class, KeySpecUtilTest.class, MlDsaTest.class, + MlKemTest.class, NativeCryptoArgTest.class, NativeCryptoTest.class, NativeSslTest.class, NativeRefTest.class, NativeSslSessionTest.class, + OpenSSLCipherTest.class, OpenSSLECGroupContextTest.class, OpenSSLKeyTest.class, OpenSSLX509CertificateTest.class, OpenSSLX509CRLTest.class, + OpenSslCompositeMlDsaKeyFactoryTest.class, + OpenSslCompositeMlDsaPrivateKeyTest.class, + OpenSslCompositeMlDsaPublicKeyTest.class, SignatureThreadMisuseTest.class, SSLUtilsTest.class, SlhDsaTest.class, @@ -111,6 +123,9 @@ // org.conscrypt.ct tests VerifierTest.class, SerializationTest.class, + // org.conscrypt.util tests + DerUtilsTest.class, + MtcUtilsTest.class, // java.security tests CertificateFactoryTest.class, X509CertificateTest.class, @@ -132,12 +147,15 @@ KeyFactoryTestDSA.class, KeyFactoryTestEC.class, KeyFactoryTestRSACrt.class, + KeyFactoryTestRSACustom.class, + KeyFactoryTestXDH.class, KeyPairGeneratorTest.class, KeyPairGeneratorTestDH.class, KeyPairGeneratorTestDSA.class, KeyPairGeneratorTestRSA.class, KeyPairGeneratorTestXDH.class, MessageDigestTest.class, + SecureRandomTest.class, SignatureTest.class, // javax.crypto tests AeadCipherTest.class, diff --git a/openjdk/src/test/java/org/conscrypt/ConscryptOpenJdkSuite.java b/openjdk/src/test/java/org/conscrypt/ConscryptOpenJdkSuite.java index b670a0a00..8115e1d7a 100644 --- a/openjdk/src/test/java/org/conscrypt/ConscryptOpenJdkSuite.java +++ b/openjdk/src/test/java/org/conscrypt/ConscryptOpenJdkSuite.java @@ -36,12 +36,15 @@ import org.conscrypt.java.security.KeyFactoryTestEC; import org.conscrypt.java.security.KeyFactoryTestRSA; import org.conscrypt.java.security.KeyFactoryTestRSACrt; +import org.conscrypt.java.security.KeyFactoryTestRSACustom; +import org.conscrypt.java.security.KeyFactoryTestXDH; import org.conscrypt.java.security.KeyPairGeneratorTest; import org.conscrypt.java.security.KeyPairGeneratorTestDH; import org.conscrypt.java.security.KeyPairGeneratorTestDSA; import org.conscrypt.java.security.KeyPairGeneratorTestRSA; import org.conscrypt.java.security.KeyPairGeneratorTestXDH; import org.conscrypt.java.security.MessageDigestTest; +import org.conscrypt.java.security.SecureRandomTest; import org.conscrypt.java.security.SignatureTest; import org.conscrypt.java.security.cert.CertificateFactoryTest; import org.conscrypt.java.security.cert.X509CRLTest; @@ -68,6 +71,7 @@ import org.conscrypt.javax.net.ssl.SSLSessionContextTest; import org.conscrypt.javax.net.ssl.SSLSessionTest; import org.conscrypt.javax.net.ssl.SSLSocketFactoryTest; +import org.conscrypt.javax.net.ssl.SSLSocketImplTest; import org.conscrypt.javax.net.ssl.SSLSocketTest; import org.conscrypt.javax.net.ssl.SSLSocketVersionCompatibilityTest; import org.conscrypt.javax.net.ssl.SslEngineAndSocketResumptionTest; @@ -76,6 +80,8 @@ import org.conscrypt.metrics.CipherSuiteTest; import org.conscrypt.metrics.OptionalMethodTest; import org.conscrypt.metrics.ProtocolTest; +import org.conscrypt.util.DerUtilsTest; +import org.conscrypt.util.MtcUtilsTest; import org.junit.BeforeClass; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -90,10 +96,13 @@ ChaCha20Poly1305Test.class, ChainStrengthAnalyzerTest.class, ClientSessionContextTest.class, + CompositeMlDsaTest.class, ConscryptSocketTest.class, ConscryptTest.class, DuckTypedHpkeSpiTest.class, DuckTypedPSKKeyManagerTest.class, + EchConfigListTest.class, + EchConfigMismatchExceptionTest.class, EdDsaTest.class, ExposedByteArrayOutputStreamTest.class, FileClientSessionCacheTest.class, @@ -104,6 +113,7 @@ HpkeContextSenderTest.class, HpkeSuiteTest.class, HpkeTestVectorsTest.class, + KeyManagerFactoryImplTest.class, KeySpecUtilTest.class, MlDsaTest.class, MlKemTest.class, @@ -112,10 +122,14 @@ NativeSslTest.class, NativeRefTest.class, NativeSslSessionTest.class, + OpenSSLCipherTest.class, OpenSSLECGroupContextTest.class, OpenSSLKeyTest.class, OpenSSLX509CertificateTest.class, OpenSSLX509CRLTest.class, + OpenSslCompositeMlDsaKeyFactoryTest.class, + OpenSslCompositeMlDsaPrivateKeyTest.class, + OpenSslCompositeMlDsaPublicKeyTest.class, PlatformTest.class, SignatureThreadMisuseTest.class, SSLUtilsTest.class, @@ -128,6 +142,9 @@ // org.conscrypt.ct tests VerifierTest.class, SerializationTest.class, + // org.conscrypt.util tests + DerUtilsTest.class, + MtcUtilsTest.class, // java.security tests CertificateFactoryTest.class, X509CertificateTest.class, @@ -150,12 +167,15 @@ KeyFactoryTestEC.class, KeyFactoryTestRSA.class, KeyFactoryTestRSACrt.class, + KeyFactoryTestRSACustom.class, + KeyFactoryTestXDH.class, KeyPairGeneratorTest.class, KeyPairGeneratorTestDH.class, KeyPairGeneratorTestDSA.class, KeyPairGeneratorTestRSA.class, KeyPairGeneratorTestXDH.class, MessageDigestTest.class, + SecureRandomTest.class, SignatureTest.class, // javax.crypto tests AeadCipherTest.class, @@ -184,6 +204,7 @@ SSLSessionContextTest.class, SSLSessionTest.class, SSLSocketFactoryTest.class, + SSLSocketImplTest.class, SSLSocketTest.class, SSLSocketVersionCompatibilityTest.class, SslEngineAndSocketResumptionTest.class, diff --git a/openjdk/src/test/java/org/conscrypt/ConscryptSocketTest.java b/openjdk/src/test/java/org/conscrypt/ConscryptSocketTest.java index b7f506640..5a010bec2 100644 --- a/openjdk/src/test/java/org/conscrypt/ConscryptSocketTest.java +++ b/openjdk/src/test/java/org/conscrypt/ConscryptSocketTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -60,6 +61,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import javax.net.ssl.HandshakeCompletedEvent; import javax.net.ssl.HandshakeCompletedListener; @@ -764,4 +766,56 @@ private byte[] randomBuffer(int size) { random.nextBytes(buffer); return buffer; } + + @Test + public void exportKeyingMaterial_afterClose() throws Exception { + TestConnection connection = new TestConnection(new X509Certificate[] {cert, ca}, certKey); + connection.doHandshakeSuccess(); + byte[] ekm = Conscrypt.exportKeyingMaterial(connection.client, "label", null, 32); + assertNotNull(ekm); + assertEquals(32, ekm.length); + + connection.client.close(); + assertNull(Conscrypt.exportKeyingMaterial(connection.client, "label", null, 32)); + } + + @Test + public void exportKeyingMaterial_concurrentClose() throws Exception { + for (int i = 0; i < 50; i++) { + TestConnection connection = + new TestConnection(new X509Certificate[] {cert, ca}, certKey); + connection.doHandshakeSuccess(); + final SSLSocket clientSocket = connection.client; + + final AtomicReference error = new AtomicReference<>(); + Thread getterThread = new Thread(() -> { + try { + for (int k = 0; k < 500; k++) { + Conscrypt.exportKeyingMaterial(clientSocket, "label", null, 32); + Conscrypt.getTlsUnique(clientSocket); + } + } catch (Throwable t) { + error.set(t); + } + }); + + Thread closerThread = new Thread(() -> { + try { + clientSocket.close(); + } catch (Throwable t) { + error.set(t); + } + }); + + getterThread.start(); + closerThread.start(); + getterThread.join(); + closerThread.join(); + + connection.server.close(); + if (error.get() != null) { + throw new AssertionError("Exception in concurrent EKM/close", error.get()); + } + } + } } diff --git a/openjdk/src/test/java/org/conscrypt/NativeCryptoTest.java b/openjdk/src/test/java/org/conscrypt/NativeCryptoTest.java index 8be0a8a9f..eea87b2a0 100644 --- a/openjdk/src/test/java/org/conscrypt/NativeCryptoTest.java +++ b/openjdk/src/test/java/org/conscrypt/NativeCryptoTest.java @@ -4564,6 +4564,131 @@ public void mlKem1024PublicKeyFromSeed_returnsPublicKeyIfPrivateKeyIsValid() thr () -> NativeCrypto.MLKEM1024_public_key_from_seed(privateKeyTooLong)); } + @Test + public void mlKem768EncapDecap_roundtrip_success() throws Exception { + byte[] privateKeySeed = new byte[64]; + NativeCrypto.RAND_bytes(privateKeySeed); + byte[] publicKey = NativeCrypto.MLKEM768_public_key_from_seed(privateKeySeed); + assertEquals(1184, publicKey.length); + + byte[] outCiphertext = new byte[1088]; + byte[] outSharedSecretSender = new byte[32]; + NativeCrypto.MLKEM768_encap(publicKey, outCiphertext, outSharedSecretSender); + + byte[] outSharedSecretRecipient = + NativeCrypto.MLKEM768_decap(outCiphertext, privateKeySeed); + assertEquals(32, outSharedSecretRecipient.length); + assertArrayEquals(outSharedSecretSender, outSharedSecretRecipient); + + // Null argument checks + assertThrows(NullPointerException.class, + () -> NativeCrypto.MLKEM768_encap(null, outCiphertext, outSharedSecretSender)); + assertThrows(NullPointerException.class, + () -> NativeCrypto.MLKEM768_encap(publicKey, null, outSharedSecretSender)); + assertThrows(NullPointerException.class, + () -> NativeCrypto.MLKEM768_encap(publicKey, outCiphertext, null)); + assertThrows(NullPointerException.class, + () -> NativeCrypto.MLKEM768_decap(null, privateKeySeed)); + assertThrows(NullPointerException.class, + () -> NativeCrypto.MLKEM768_decap(outCiphertext, null)); + + // Invalid encap lengths (length - 1 and length + 1) + assertThrows(IllegalArgumentException.class, + () + -> NativeCrypto.MLKEM768_encap(new byte[1183], outCiphertext, + outSharedSecretSender)); + assertThrows(IllegalArgumentException.class, + () + -> NativeCrypto.MLKEM768_encap(new byte[1185], outCiphertext, + outSharedSecretSender)); + assertThrows(IllegalArgumentException.class, + () + -> NativeCrypto.MLKEM768_encap(publicKey, new byte[1087], + outSharedSecretSender)); + assertThrows(IllegalArgumentException.class, + () + -> NativeCrypto.MLKEM768_encap(publicKey, new byte[1089], + outSharedSecretSender)); + assertThrows(IllegalArgumentException.class, + () -> NativeCrypto.MLKEM768_encap(publicKey, outCiphertext, new byte[31])); + assertThrows(IllegalArgumentException.class, + () -> NativeCrypto.MLKEM768_encap(publicKey, outCiphertext, new byte[33])); + + // Invalid ciphertext length (length - 1 and length + 1) + assertThrows(RuntimeException.class, + () -> NativeCrypto.MLKEM768_decap(new byte[1087], privateKeySeed)); + assertThrows(RuntimeException.class, + () -> NativeCrypto.MLKEM768_decap(new byte[1089], privateKeySeed)); + // Invalid seed length (length - 1 and length + 1) + assertThrows(RuntimeException.class, + () -> NativeCrypto.MLKEM768_decap(outCiphertext, new byte[63])); + assertThrows(RuntimeException.class, + () -> NativeCrypto.MLKEM768_decap(outCiphertext, new byte[65])); + } + + @Test + public void mlKem1024EncapDecap_roundtrip_success() throws Exception { + byte[] privateKeySeed = new byte[64]; + NativeCrypto.RAND_bytes(privateKeySeed); + byte[] publicKey = NativeCrypto.MLKEM1024_public_key_from_seed(privateKeySeed); + assertEquals(1568, publicKey.length); + + byte[] outCiphertext = new byte[1568]; + byte[] outSharedSecretSender = new byte[32]; + NativeCrypto.MLKEM1024_encap(publicKey, outCiphertext, outSharedSecretSender); + + byte[] outSharedSecretRecipient = + NativeCrypto.MLKEM1024_decap(outCiphertext, privateKeySeed); + assertEquals(32, outSharedSecretRecipient.length); + assertArrayEquals(outSharedSecretSender, outSharedSecretRecipient); + + // Null argument checks + assertThrows( + NullPointerException.class, + () -> NativeCrypto.MLKEM1024_encap(null, outCiphertext, outSharedSecretSender)); + assertThrows(NullPointerException.class, + () -> NativeCrypto.MLKEM1024_encap(publicKey, null, outSharedSecretSender)); + assertThrows(NullPointerException.class, + () -> NativeCrypto.MLKEM1024_encap(publicKey, outCiphertext, null)); + assertThrows(NullPointerException.class, + () -> NativeCrypto.MLKEM1024_decap(null, privateKeySeed)); + assertThrows(NullPointerException.class, + () -> NativeCrypto.MLKEM1024_decap(outCiphertext, null)); + + // Invalid encap lengths (length - 1 and length + 1) + assertThrows(IllegalArgumentException.class, + () + -> NativeCrypto.MLKEM1024_encap(new byte[1567], outCiphertext, + outSharedSecretSender)); + assertThrows(IllegalArgumentException.class, + () + -> NativeCrypto.MLKEM1024_encap(new byte[1569], outCiphertext, + outSharedSecretSender)); + assertThrows(IllegalArgumentException.class, + () + -> NativeCrypto.MLKEM1024_encap(publicKey, new byte[1567], + outSharedSecretSender)); + assertThrows(IllegalArgumentException.class, + () + -> NativeCrypto.MLKEM1024_encap(publicKey, new byte[1569], + outSharedSecretSender)); + assertThrows(IllegalArgumentException.class, + () -> NativeCrypto.MLKEM1024_encap(publicKey, outCiphertext, new byte[31])); + assertThrows(IllegalArgumentException.class, + () -> NativeCrypto.MLKEM1024_encap(publicKey, outCiphertext, new byte[33])); + + // Invalid ciphertext length (length - 1 and length + 1) + assertThrows(RuntimeException.class, + () -> NativeCrypto.MLKEM1024_decap(new byte[1567], privateKeySeed)); + assertThrows(RuntimeException.class, + () -> NativeCrypto.MLKEM1024_decap(new byte[1569], privateKeySeed)); + // Invalid seed length (length - 1 and length + 1) + assertThrows(RuntimeException.class, + () -> NativeCrypto.MLKEM1024_decap(outCiphertext, new byte[63])); + assertThrows(RuntimeException.class, + () -> NativeCrypto.MLKEM1024_decap(outCiphertext, new byte[65])); + } + @Test public void xwingPublicKeyFromSeed_returnsPublicKeyIfPrivateKeyIsValid() throws Exception { // test vector from diff --git a/openjdk/src/test/java/org/conscrypt/NativeSslTest.java b/openjdk/src/test/java/org/conscrypt/NativeSslTest.java index 37d2b5e6b..3cccda345 100644 --- a/openjdk/src/test/java/org/conscrypt/NativeSslTest.java +++ b/openjdk/src/test/java/org/conscrypt/NativeSslTest.java @@ -16,6 +16,7 @@ package org.conscrypt; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; @@ -23,6 +24,10 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; + @RunWith(JUnit4.class) public final class NativeSslTest { @Test @@ -104,34 +109,140 @@ public void parseNamedGroupsProperty_throwsIfNoKnownGroupsFound() { @Test public void parseTlsNamedGroupsProperty_works() throws Exception { String savedProperty = System.getProperty("jdk.tls.namedGroups"); + try { + // Valid values. + System.setProperty("jdk.tls.namedGroups", "P-384,X25519"); + NativeSsl.parseTlsNamedGroupsProperty(); + assertArrayEquals(new int[] {NativeConstants.NID_secp384r1, NativeConstants.NID_X25519}, + NativeSsl.getParsedTlsNamedGroupsPropertyOrNull()); - // Valid values. - System.setProperty("jdk.tls.namedGroups", "P-384,X25519"); - NativeSsl.parseTlsNamedGroupsProperty(); - assertArrayEquals(new int[] {NativeConstants.NID_secp384r1, NativeConstants.NID_X25519}, - NativeSsl.getParsedTlsNamedGroupsPropertyOrNull()); + // Property not set. + System.clearProperty("jdk.tls.namedGroups"); + NativeSsl.parseTlsNamedGroupsProperty(); + assertNull(NativeSsl.getParsedTlsNamedGroupsPropertyOrNull()); - // Property not set. - System.clearProperty("jdk.tls.namedGroups"); - NativeSsl.parseTlsNamedGroupsProperty(); - assertNull(NativeSsl.getParsedTlsNamedGroupsPropertyOrNull()); + // Empty property. + System.setProperty("jdk.tls.namedGroups", ""); + NativeSsl.parseTlsNamedGroupsProperty(); + assertNull(NativeSsl.getParsedTlsNamedGroupsPropertyOrNull()); - // Empty property. - System.setProperty("jdk.tls.namedGroups", ""); - NativeSsl.parseTlsNamedGroupsProperty(); - assertNull(NativeSsl.getParsedTlsNamedGroupsPropertyOrNull()); + // Invalid values (calling repeatedly must continue to throw rather than caching invalid + // property). + System.setProperty("jdk.tls.namedGroups", "invalid,invalid2"); + assertThrows(IllegalArgumentException.class, + () -> NativeSsl.parseTlsNamedGroupsProperty()); + assertThrows(IllegalArgumentException.class, + () -> NativeSsl.parseTlsNamedGroupsProperty()); + assertNull(NativeSsl.getParsedTlsNamedGroupsPropertyOrNull()); + } finally { + // Restore the property to its original value, to make sure that the test does not + // have any side effects on other tests when setting the property. + if (savedProperty == null) { + System.clearProperty("jdk.tls.namedGroups"); + } else { + System.setProperty("jdk.tls.namedGroups", savedProperty); + } + NativeSsl.getParsedTlsNamedGroupsPropertyOrNull(); + } + } - // Invalid values. - System.setProperty("jdk.tls.namedGroups", "invalid,invalid2"); - assertThrows(IllegalArgumentException.class, () -> NativeSsl.parseTlsNamedGroupsProperty()); + @Test + public void staticInitializer_doesNotFailWhenNamedGroupsPropertyContainsOnlyUnrecognizedGroups() + throws Exception { + String savedProperty = System.getProperty("jdk.tls.namedGroups"); + try { + System.setProperty("jdk.tls.namedGroups", "UnknownGroup1,UnknownGroup2"); + Class isolatedNativeSsl = loadAndInitializeNativeSslInIsolatedClassLoader(); + assertNotNull(isolatedNativeSsl); + } finally { + if (savedProperty == null) { + System.clearProperty("jdk.tls.namedGroups"); + } else { + System.setProperty("jdk.tls.namedGroups", savedProperty); + } + NativeSsl.getParsedTlsNamedGroupsPropertyOrNull(); + } + } - // Restore the property to its original value, to make sure that the test does not - // have any side effects on other tests when setting the property. - if (savedProperty == null) { - System.clearProperty("jdk.tls.namedGroups"); - } else { - System.setProperty("jdk.tls.namedGroups", savedProperty); + private static Class loadAndInitializeNativeSslInIsolatedClassLoader() throws Exception { + final String className = NativeSsl.class.getName(); + final String resourceName = "NativeSsl.class"; + try (InputStream is = NativeSsl.class.getResourceAsStream(resourceName)) { + if (is != null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int read; + while ((read = is.read(buffer)) != -1) { + baos.write(buffer, 0, read); + } + final byte[] classBytes = baos.toByteArray(); + ClassLoader loader = new ClassLoader(NativeSsl.class.getClassLoader()) { + @Override + protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + if (name.equals(className)) { + Class c = findLoadedClass(name); + if (c == null) { + c = defineClass(name, classBytes, 0, classBytes.length, + NativeSsl.class.getProtectionDomain()); + } + if (resolve) { + resolveClass(c); + } + return c; + } + return super.loadClass(name, resolve); + } + }; + return Class.forName(className, /* initialize= */ true, loader); + } + } + // On Android ART, classes are in DEX format rather than .class resources. + // Include any APK/JAR dex paths from NativeSsl's ClassLoader (needed for unbundled APK + // instrumentation tests where java.class.path is just "."), and delegate all classes except + // NativeSsl itself to NativeSsl.class.getClassLoader(). + StringBuilder dexPathBuilder = new StringBuilder(); + ClassLoader sourceLoader = NativeSsl.class.getClassLoader(); + if (sourceLoader != null) { + String loaderStr = sourceLoader.toString(); + int idx = 0; + while ((idx = loaderStr.indexOf("zip file \"", idx)) != -1) { + int start = idx + "zip file \"".length(); + int end = loaderStr.indexOf('"', start); + if (end > start) { + if (dexPathBuilder.length() > 0) { + dexPathBuilder.append(File.pathSeparator); + } + dexPathBuilder.append(loaderStr, start, end); + idx = end + 1; + } else { + break; + } + } + } + for (String prop : new String[] {"java.class.path", "java.boot.class.path"}) { + String val = System.getProperty(prop, ""); + if (!val.isEmpty()) { + if (dexPathBuilder.length() > 0) { + dexPathBuilder.append(File.pathSeparator); + } + dexPathBuilder.append(val); + } } - NativeSsl.parseTlsNamedGroupsProperty(); + ClassLoader delegatingParent = new ClassLoader(sourceLoader) { + @Override + protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + if (name.equals(className)) { + throw new ClassNotFoundException(name); + } + return super.loadClass(name, resolve); + } + }; + Class pathClassLoaderClass = Class.forName("dalvik.system.PathClassLoader"); + ClassLoader dexLoader = + (ClassLoader) pathClassLoaderClass.getConstructor(String.class, ClassLoader.class) + .newInstance(dexPathBuilder.toString(), delegatingParent); + return Class.forName(className, /* initialize= */ true, dexLoader); } } diff --git a/openjdk/src/test/java/org/conscrypt/OpenSSLCipherTest.java b/openjdk/src/test/java/org/conscrypt/OpenSSLCipherTest.java new file mode 100644 index 000000000..b4fdad4c0 --- /dev/null +++ b/openjdk/src/test/java/org/conscrypt/OpenSSLCipherTest.java @@ -0,0 +1,678 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * 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 org.conscrypt; + +import static org.conscrypt.TestUtils.UTF_8; +import static org.conscrypt.TestUtils.decodeHex; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.security.InvalidKeyException; +import java.security.Provider; +import java.security.spec.AlgorithmParameterSpec; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +@RunWith(JUnit4.class) +public class OpenSSLCipherTest { + private static final Provider CONSCRYPT_PROVIDER = TestUtils.getConscryptProvider(); + + // Fixed keys: bytes 0x00..0x0f (16B), 0x00..0x17 (24B), 0x00..0x1f (32B) + private static final byte[] key16 = decodeHex("000102030405060708090a0b0c0d0e0f"); + private static final byte[] key24 = + decodeHex("000102030405060708090a0b0c0d0e0f1011121314151617"); + private static final byte[] key32 = + decodeHex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"); + + // Fixed IVs: 16B (0x10..0x1f), 12B (0x20..0x2b), 8B (0x2c..0x33), 24B (0x20..0x37) + private static final byte[] iv16 = decodeHex("101112131415161718191a1b1c1d1e1f"); + private static final byte[] iv12 = decodeHex("202122232425262728292a2b"); + private static final byte[] iv8 = decodeHex("2c2d2e2f30313233"); + private static final byte[] iv24 = + decodeHex("202122232425262728292a2b2c2d2e2f3031323334353637"); + + // Expected plaintexts + private static final byte[] plaintext16 = "0123456789abcdef".getBytes(UTF_8); + private static final byte[] plaintext8 = "01234567".getBytes(UTF_8); + private static final byte[] plaintextVar = "Hello, Conscrypt!".getBytes(UTF_8); + + // Hard-coded ciphertexts for the fixed keys, IVs, and plaintexts above + private static final byte[] ciphertextAes128EcbNopad = + decodeHex("281567ab2f4cf0d73d3198225b8b8393"); + private static final byte[] ciphertextAes192EcbNopad = + decodeHex("d2a70d53d18493c560e5a1d505d2e23c"); + private static final byte[] ciphertextAes256EcbNopad = + decodeHex("d8c95758e3353e530fa52bd10e73b986"); + + private static final byte[] ciphertextAes128EcbPkcs5 = + decodeHex("bd361e6a4c0aad7f724ce34310305771ec19c77a6de4cfedcef118e23f0be27e"); + private static final byte[] ciphertextAes192EcbPkcs5 = + decodeHex("eca30916505a875a67f365f9c030be1bb4e5f73fbc7baf5aa9f913480e3a975b"); + private static final byte[] ciphertextAes256EcbPkcs5 = + decodeHex("bb6e3ec15f5fbf3eeba5ec6edd0368c2caef675bce76924d1d7aa70dc1b15f34"); + + private static final byte[] ciphertextAes128CbcNopad = + decodeHex("eb9e5ba41b902db8252982aa1a23f4be"); + private static final byte[] ciphertextAes192CbcNopad = + decodeHex("93c788f330932c697cd70aa68bd01a82"); + private static final byte[] ciphertextAes256CbcNopad = + decodeHex("879adb94b99e35f6f6d9680548bb71d1"); + + private static final byte[] ciphertextAes128CbcPkcs5 = + decodeHex("bdff9f646b327909d89267efd062f844d57589515c4a97189d78ef2901acc071"); + private static final byte[] ciphertextAes192CbcPkcs5 = + decodeHex("477fbc72b2d1dadd9d962737faee163d9b129ca32386828e9fdd02c2bfbd70c9"); + private static final byte[] ciphertextAes256CbcPkcs5 = + decodeHex("c24df64088a8441e87c748ed9293c7061284b1838bb245320fbcd960da04c891"); + + private static final byte[] ciphertextAes128CtrNopad = + decodeHex("4f9b83188ef9232dff609d72fcede2e7b3"); + private static final byte[] ciphertextAes192CtrNopad = + decodeHex("dbcb5713f0eec856f26bd5ca879b3f59da"); + private static final byte[] ciphertextAes256CtrNopad = + decodeHex("a1a683e6dd1873a59f1aefb5449ed8fad8"); + + private static final byte[] ciphertextDesede16CbcNopad = decodeHex("455d5301f20d180d"); + private static final byte[] ciphertextDesede24CbcNopad = decodeHex("61935a9f5bf72b04"); + private static final byte[] ciphertextDesede16CbcPkcs5 = + decodeHex("c843bbb8630bb60e883242edce272b2aa1bd606dbae4fafc"); + private static final byte[] ciphertextDesede24CbcPkcs5 = + decodeHex("5997754a0f569a40c3a6dcd73a0ec591bf09d61f4a452b78"); + + private static final byte[] ciphertextArc4 = decodeHex("a1f92c9528ce398f69b5e4a57ca45a3bf2"); + + private static final byte[] ciphertextAes128Gcm = + decodeHex("88907ec8e9dc594ad426fcb703b6e40ae91eca5c8447a37c278cbc032780714159"); + private static final byte[] ciphertextAes256Gcm = + decodeHex("9a5fca1c03b43a4d751231adb361848df194f008dd6916e85e10d81b12f2d14173"); + + private static final byte[] ciphertextAes128GcmSiv = + decodeHex("5bc107b4d1677192bbc5fa7fdab0b29731b96f294a81f90477361fb67713955848"); + private static final byte[] ciphertextAes256GcmSiv = + decodeHex("6d8732d6258aae69bd2115e0d101da7f8d9bdd2a4dcaf960011b7c6492fd935c4c"); + + private static final byte[] ciphertextChaCha20 = + decodeHex("dbb73b8876f6da1d59a5fca3bd41efe411"); + private static final byte[] ciphertextChaCha20Poly1305 = + decodeHex("3b82e4107ec3dc79f4abdeab3b46ffcc5a3ee281de608eecdc42b24c97abde66c4"); + private static final byte[] ciphertextXChaCha20Poly1305 = + decodeHex("553c21a7141b34c857d023de3a3e71a51c8b62ec80526aaffea5e4f457dd9f59f7"); + + @BeforeClass + public static void setUp() { + TestUtils.assumeAllowsUnsignedCrypto(); + } + + private static void assertDecryptsToExpectedPlaintext( + String transformation, String keyAlg, byte[] keyBytes, AlgorithmParameterSpec paramSpec, + byte[] ciphertext, byte[] expectedPlaintext) throws Exception { + Cipher cipher = Cipher.getInstance(transformation, CONSCRYPT_PROVIDER); + assertEquals(CONSCRYPT_PROVIDER, cipher.getProvider()); + SecretKey key = new SecretKeySpec(keyBytes, keyAlg); + if (paramSpec == null) { + cipher.init(Cipher.DECRYPT_MODE, key); + } else { + cipher.init(Cipher.DECRYPT_MODE, key, paramSpec); + } + byte[] decrypted = cipher.doFinal(ciphertext); + assertArrayEquals(expectedPlaintext, decrypted); + } + + private static void assertRejectsKey(String transformation, String keyAlg, byte[] keyBytes, + AlgorithmParameterSpec paramSpec) throws Exception { + Cipher cipher = Cipher.getInstance(transformation, CONSCRYPT_PROVIDER); + assertEquals(CONSCRYPT_PROVIDER, cipher.getProvider()); + SecretKey key = new SecretKeySpec(keyBytes, keyAlg); + if (paramSpec == null) { + assertThrows(InvalidKeyException.class, () -> cipher.init(Cipher.ENCRYPT_MODE, key)); + assertThrows(InvalidKeyException.class, () -> cipher.init(Cipher.DECRYPT_MODE, key)); + } else { + assertThrows(InvalidKeyException.class, + () -> cipher.init(Cipher.ENCRYPT_MODE, key, paramSpec)); + assertThrows(InvalidKeyException.class, + () -> cipher.init(Cipher.DECRYPT_MODE, key, paramSpec)); + } + } + + // ========================================================================= + // 1. Cipher.getInstance + Hard-coded Ciphertext Decryption Tests + // for all symmetric cipher algorithms and aliases in OpenSSLProvider + // ========================================================================= + + @Test + public void decrypt_aesEcbNoPadding() throws Exception { + assertDecryptsToExpectedPlaintext("AES/ECB/NoPadding", "AES", key16, null, + ciphertextAes128EcbNopad, plaintext16); + assertDecryptsToExpectedPlaintext("AES/ECB/NoPadding", "AES", key24, null, + ciphertextAes192EcbNopad, plaintext16); + assertDecryptsToExpectedPlaintext("AES/ECB/NoPadding", "AES", key32, null, + ciphertextAes256EcbNopad, plaintext16); + assertDecryptsToExpectedPlaintext("Aes/EcB/noPadDing", "aEs", key32, null, + ciphertextAes256EcbNopad, plaintext16); + } + + @Test + public void decrypt_aesEcbPkcs5Padding_andAliases() throws Exception { + for (String transformation : new String[] {"AES/ECB/PKCS5Padding", "AES/ECB/PKCS7Padding", + "Aes/EcB/pkcs5Padding"}) { + assertDecryptsToExpectedPlaintext(transformation, "AES", key16, null, + ciphertextAes128EcbPkcs5, plaintextVar); + assertDecryptsToExpectedPlaintext(transformation, "AES", key24, null, + ciphertextAes192EcbPkcs5, plaintextVar); + assertDecryptsToExpectedPlaintext(transformation, "AES", key32, null, + ciphertextAes256EcbPkcs5, plaintextVar); + assertDecryptsToExpectedPlaintext(transformation, "aEs", key16, null, + ciphertextAes128EcbPkcs5, plaintextVar); + } + } + + @Test + public void decrypt_aesCbcNoPadding() throws Exception { + IvParameterSpec iv = new IvParameterSpec(iv16); + assertDecryptsToExpectedPlaintext("AES/CBC/NoPadding", "AES", key16, iv, + ciphertextAes128CbcNopad, plaintext16); + assertDecryptsToExpectedPlaintext("AES/CBC/NoPadding", "AES", key24, iv, + ciphertextAes192CbcNopad, plaintext16); + assertDecryptsToExpectedPlaintext("AES/CBC/NoPadding", "AES", key32, iv, + ciphertextAes256CbcNopad, plaintext16); + assertDecryptsToExpectedPlaintext("Aes/CbC/noPadding", "aEs", key32, iv, + ciphertextAes256CbcNopad, plaintext16); + } + + @Test + public void decrypt_aesCbcPkcs5Padding_andAliases() throws Exception { + IvParameterSpec iv = new IvParameterSpec(iv16); + for (String transformation : new String[] {"AES/CBC/PKCS5Padding", "AES/CBC/PKCS7Padding", + "Aes/CbC/pkcs5Padding"}) { + assertDecryptsToExpectedPlaintext(transformation, "AES", key16, iv, + ciphertextAes128CbcPkcs5, plaintextVar); + assertDecryptsToExpectedPlaintext(transformation, "AES", key24, iv, + ciphertextAes192CbcPkcs5, plaintextVar); + assertDecryptsToExpectedPlaintext(transformation, "AES", key32, iv, + ciphertextAes256CbcPkcs5, plaintextVar); + assertDecryptsToExpectedPlaintext(transformation, "aEs", key16, iv, + ciphertextAes128CbcPkcs5, plaintextVar); + } + } + + @Test + public void decrypt_aesCtrNoPadding() throws Exception { + IvParameterSpec iv = new IvParameterSpec(iv16); + assertDecryptsToExpectedPlaintext("AES/CTR/NoPadding", "AES", key16, iv, + ciphertextAes128CtrNopad, plaintextVar); + assertDecryptsToExpectedPlaintext("AES/CTR/NoPadding", "AES", key24, iv, + ciphertextAes192CtrNopad, plaintextVar); + assertDecryptsToExpectedPlaintext("AES/CTR/NoPadding", "AES", key32, iv, + ciphertextAes256CtrNopad, plaintextVar); + assertDecryptsToExpectedPlaintext("aEs/CtR/noPadding", "aEs", key32, iv, + ciphertextAes256CtrNopad, plaintextVar); + } + + @Test + public void decrypt_aes128EcbNoPadding() throws Exception { + assertDecryptsToExpectedPlaintext("AES_128/ECB/NoPadding", "AES", key16, null, + ciphertextAes128EcbNopad, plaintext16); + assertDecryptsToExpectedPlaintext("aEs_128/EcB/noPadding", "aEs", key16, null, + ciphertextAes128EcbNopad, plaintext16); + } + + @Test + public void decrypt_aes128EcbPkcs5Padding_andAliases() throws Exception { + for (String transformation : + new String[] {"AES_128/ECB/PKCS5Padding", "AES_128/ECB/PKCS7Padding", + "aEs_128/EcB/pkcs5Padding"}) { + assertDecryptsToExpectedPlaintext(transformation, "AES", key16, null, + ciphertextAes128EcbPkcs5, plaintextVar); + assertDecryptsToExpectedPlaintext(transformation, "aEs", key16, null, + ciphertextAes128EcbPkcs5, plaintextVar); + } + } + + @Test + public void decrypt_aes128CbcNoPadding() throws Exception { + assertDecryptsToExpectedPlaintext("AES_128/CBC/NoPadding", "AES", key16, + new IvParameterSpec(iv16), ciphertextAes128CbcNopad, + plaintext16); + assertDecryptsToExpectedPlaintext("aEs_128/CbC/noPadding", "aEs", key16, + new IvParameterSpec(iv16), ciphertextAes128CbcNopad, + plaintext16); + } + + @Test + public void decrypt_aes128CbcPkcs5Padding_andAliases() throws Exception { + IvParameterSpec iv = new IvParameterSpec(iv16); + String[] transformations = { + "AES_128/CBC/PKCS5Padding", + "AES_128/CBC/PKCS7Padding", + "PBEWithHmacSHA1AndAES_128", + "PBEWithHmacSHA224AndAES_128", + "PBEWithHmacSHA256AndAES_128", + "PBEWithHmacSHA384AndAES_128", + "PBEWithHmacSHA512AndAES_128", + "aEs_128/cbc/pkcs5Padding", + // Adding Mode and padding to alias also works. + "PBEWithHmacSHA1AndAES_128/CBC/PKCS5Padding", + "pbeWithHmacSHA1AndAES_128/cbc/pkcs5Padding", + + }; + for (String transformation : transformations) { + assertDecryptsToExpectedPlaintext(transformation, "AES", key16, iv, + ciphertextAes128CbcPkcs5, plaintextVar); + assertDecryptsToExpectedPlaintext(transformation, "aEs", key16, iv, + ciphertextAes128CbcPkcs5, plaintextVar); + } + } + + @Test + public void decrypt_pbeWithHmacSHA1AndAES128_withEcbMode() throws Exception { + // PBEWithHmacSHA1AndAES_128 is an alias for AES_128/CBC/PKCS5PADDING, but + // the Cipher.getInstance allows to use a different mode/padding. So + // PBEWithHmacSHA1AndAES_128/ECB/PKCS5Padding is an alias for + // AES_128/ECB/PKCS5Padding and PBEWithHmacSHA1AndAES_128/ECB/NoPadding is an alias for + // AES_128/ECB/NoPadding. + assertDecryptsToExpectedPlaintext("PBEWithHmacSHA1AndAES_128/ECB/PKCS5Padding", "AES", + key16, null, ciphertextAes128EcbPkcs5, plaintextVar); + assertDecryptsToExpectedPlaintext("PBEWithHmacSHA1AndAES_128/ECB/PKCS7Padding", "AES", + key16, null, ciphertextAes128EcbPkcs5, plaintextVar); + assertDecryptsToExpectedPlaintext("PBEWithHmacSHA1AndAES_128/ECB/NoPadding", "AES", + key16, null, ciphertextAes128EcbNopad, plaintext16); + assertDecryptsToExpectedPlaintext("pbeWithHmacSHA1AndAES_128/ecb/pkcs5Padding", "aEs", + key16, null, ciphertextAes128EcbPkcs5, plaintextVar); + assertDecryptsToExpectedPlaintext("pbeWithHmacSHA1AndAES_128/ecb/nopadding", "aEs", + key16, null, ciphertextAes128EcbNopad, plaintext16); + } + + @Test + public void decrypt_aes256EcbNoPadding() throws Exception { + assertDecryptsToExpectedPlaintext("AES_256/ECB/NoPadding", "AES", key32, null, + ciphertextAes256EcbNopad, plaintext16); + assertDecryptsToExpectedPlaintext("aEs_256/EcB/noPadding", "aEs", key32, null, + ciphertextAes256EcbNopad, plaintext16); + } + + @Test + public void decrypt_aes256EcbPkcs5Padding_andAliases() throws Exception { + for (String transformation : + new String[] {"AES_256/ECB/PKCS5Padding", "AES_256/ECB/PKCS7Padding", + "aEs_256/EcB/pkcs5Padding"}) { + assertDecryptsToExpectedPlaintext(transformation, "AES", key32, null, + ciphertextAes256EcbPkcs5, plaintextVar); + assertDecryptsToExpectedPlaintext(transformation, "aEs", key32, null, + ciphertextAes256EcbPkcs5, plaintextVar); + } + } + + @Test + public void decrypt_aes256CbcNoPadding() throws Exception { + assertDecryptsToExpectedPlaintext("AES_256/CBC/NoPadding", "AES", key32, + new IvParameterSpec(iv16), ciphertextAes256CbcNopad, + plaintext16); + assertDecryptsToExpectedPlaintext("aEs_256/CbC/noPadding", "aEs", key32, + new IvParameterSpec(iv16), ciphertextAes256CbcNopad, + plaintext16); + } + + @Test + public void decrypt_aes256CbcPkcs5Padding_andAliases() throws Exception { + IvParameterSpec iv = new IvParameterSpec(iv16); + String[] transformations = {"AES_256/CBC/PKCS5Padding", "AES_256/CBC/PKCS7Padding", + "PBEWithHmacSHA1AndAES_256", "PBEWithHmacSHA224AndAES_256", + "PBEWithHmacSHA256AndAES_256", "PBEWithHmacSHA384AndAES_256", + "PBEWithHmacSHA512AndAES_256", "aEs_256/cbc/pkcs5Padding", + // Adding Mode and padding to alias also works. + "PBEWithHmacSHA1AndAES_256/CBC/PKCS5Padding", + "pbeWithHmacSHA1AndAES_256/cbc/pkcs5Padding"}; + for (String transformation : transformations) { + assertDecryptsToExpectedPlaintext(transformation, "AES", key32, iv, + ciphertextAes256CbcPkcs5, plaintextVar); + assertDecryptsToExpectedPlaintext(transformation, "aEs", key32, iv, + ciphertextAes256CbcPkcs5, plaintextVar); + } + } + + @Test + public void decrypt_pbeWithHmacSHA1AndAES256_withEcbMode() throws Exception { + // PBEWithHmacSHA1AndAES_256 is an alias for AES_256/CBC/PKCS5PADDING, but + // the Cipher.getInstance allows to use a different mode/padding. + assertDecryptsToExpectedPlaintext("PBEWithHmacSHA1AndAES_256/ECB/PKCS5Padding", "AES", + key32, null, ciphertextAes256EcbPkcs5, plaintextVar); + } + + @Test + public void decrypt_desedeCbcNoPadding() throws Exception { + IvParameterSpec iv = new IvParameterSpec(iv8); + assertDecryptsToExpectedPlaintext("DESEDE/CBC/NoPadding", "DESEDE", key16, iv, + ciphertextDesede16CbcNopad, plaintext8); + assertDecryptsToExpectedPlaintext("DESEDE/CBC/NoPadding", "DESEDE", key24, iv, + ciphertextDesede24CbcNopad, plaintext8); + } + + @Test + public void decrypt_desedeCbcPkcs5Padding_andAliases() throws Exception { + IvParameterSpec iv = new IvParameterSpec(iv8); + for (String transformation : + new String[] {"DESEDE/CBC/PKCS5Padding", "DESEDE/CBC/PKCS7Padding"}) { + assertDecryptsToExpectedPlaintext(transformation, "DESEDE", key16, iv, + ciphertextDesede16CbcPkcs5, plaintextVar); + assertDecryptsToExpectedPlaintext(transformation, "DESEDE", key24, iv, + ciphertextDesede24CbcPkcs5, plaintextVar); + } + } + + @Test + public void decrypt_arc4_andAliases() throws Exception { + String[] transformations = { + "ARC4", + "ARCFOUR", + "RC4", + "1.2.840.113549.3.4", + "OID.1.2.840.113549.3.4", + "aRc4", + "arcFour", + "rC4", + // Adding Mode and padding to alias also works. + "ARC4/NONE/NoPadding", + "ARCFOUR/ECB/NoPadding", + "RC4/NONE/NoPadding", + "1.2.840.113549.3.4/NONE/NoPadding", + "OID.1.2.840.113549.3.4/ECB/NoPadding", + }; + for (String transformation : transformations) { + assertDecryptsToExpectedPlaintext(transformation, "ARC4", key16, null, ciphertextArc4, + plaintextVar); + } + } + + @Test + public void decrypt_aesGcmNoPadding_andAliases() throws Exception { + GCMParameterSpec spec = new GCMParameterSpec(128, iv12); + String[] transformations = { + "AES/GCM/NoPadding", + "GCM", + "aEs/gcm/noPadDing", + "gcM", + // OIDs for AES-128-GCM, AES-192-GCM and AES-192-GCM. + // But they all accept 16-byte and 32-byte keys, but not 24-byte keys. + "2.16.840.1.101.3.4.1.6", + "2.16.840.1.101.3.4.1.26", + "2.16.840.1.101.3.4.1.46", + // Adding Mode and padding to alias also works. + "GCM/GCM/NoPadding", + "gcm/gcm/nopadding", + "2.16.840.1.101.3.4.1.6/GCM/NoPadding", + "2.16.840.1.101.3.4.1.26/GCM/NoPadding", + "2.16.840.1.101.3.4.1.46/GCM/NoPadding", + }; + for (String transformation : transformations) { + assertDecryptsToExpectedPlaintext(transformation, "AES", key16, spec, + ciphertextAes128Gcm, plaintextVar); + assertDecryptsToExpectedPlaintext(transformation, "AES", key32, spec, + ciphertextAes256Gcm, plaintextVar); + } + } + + @Test + public void decrypt_aes128GcmNoPadding() throws Exception { + assertDecryptsToExpectedPlaintext("AES_128/GCM/NoPadding", "AES", key16, + new GCMParameterSpec(128, iv12), ciphertextAes128Gcm, + plaintextVar); + } + + @Test + public void decrypt_aes256GcmNoPadding() throws Exception { + assertDecryptsToExpectedPlaintext("AES_256/GCM/NoPadding", "AES", key32, + new GCMParameterSpec(128, iv12), ciphertextAes256Gcm, + plaintextVar); + assertDecryptsToExpectedPlaintext("aEs_256/gcm/noPadding", "aEs", key32, + new GCMParameterSpec(128, iv12), ciphertextAes256Gcm, + plaintextVar); + } + + @Test + public void decrypt_aesGcmSivNoPadding() throws Exception { + GCMParameterSpec spec = new GCMParameterSpec(128, iv12); + assertDecryptsToExpectedPlaintext("AES/GCM-SIV/NoPadding", "AES", key16, spec, + ciphertextAes128GcmSiv, plaintextVar); + assertDecryptsToExpectedPlaintext("AES/GCM-SIV/NoPadding", "AES", key32, spec, + ciphertextAes256GcmSiv, plaintextVar); + assertDecryptsToExpectedPlaintext("aEs/gcm-siv/noPadding", "aEs", key16, spec, + ciphertextAes128GcmSiv, plaintextVar); + assertDecryptsToExpectedPlaintext("aEs/gcm-siv/noPadding", "aEs", key32, spec, + ciphertextAes256GcmSiv, plaintextVar); + } + + @Test + public void decrypt_aes128GcmSivNoPadding() throws Exception { + assertDecryptsToExpectedPlaintext("AES_128/GCM-SIV/NoPadding", "AES", key16, + new GCMParameterSpec(128, iv12), ciphertextAes128GcmSiv, + plaintextVar); + assertDecryptsToExpectedPlaintext("aEs_128/gcm-siv/noPadding", "aEs", key16, + new GCMParameterSpec(128, iv12), ciphertextAes128GcmSiv, + plaintextVar); + } + + @Test + public void decrypt_aes256GcmSivNoPadding() throws Exception { + assertDecryptsToExpectedPlaintext("AES_256/GCM-SIV/NoPadding", "AES", key32, + new GCMParameterSpec(128, iv12), ciphertextAes256GcmSiv, + plaintextVar); + assertDecryptsToExpectedPlaintext("aEs_256/gcm-siv/noPadding", "aEs", key32, + new GCMParameterSpec(128, iv12), ciphertextAes256GcmSiv, + plaintextVar); + } + + @Test + public void decrypt_chaCha20() throws Exception { + assertDecryptsToExpectedPlaintext("ChaCha20", "ChaCha20", key32, new IvParameterSpec(iv12), + ciphertextChaCha20, plaintextVar); + assertDecryptsToExpectedPlaintext("cHaCha20", "cHaCha20", key32, new IvParameterSpec(iv12), + ciphertextChaCha20, plaintextVar); + } + + @Test + public void decrypt_chaCha20Poly1305_andAliases() throws Exception { + IvParameterSpec iv = new IvParameterSpec(iv12); + for (String transformation : new String[] { + "ChaCha20/Poly1305/NoPadding", + "ChaCha20-Poly1305", + "chacha20/poly1305/nopadding", + // Adding Mode and padding to alias also works. + "ChaCha20-Poly1305/Poly1305/NoPadding", + "chacha20-poly1305/poly1305/nopadding", + }) { + assertDecryptsToExpectedPlaintext(transformation, "ChaCha20", key32, iv, + ciphertextChaCha20Poly1305, plaintextVar); + assertDecryptsToExpectedPlaintext(transformation, "chacha20", key32, iv, + ciphertextChaCha20Poly1305, plaintextVar); + } + } + + @Test + public void decrypt_xChaCha20Poly1305_andAliases() throws Exception { + IvParameterSpec iv = new IvParameterSpec(iv24); + for (String transformation : new String[] { + "XChaCha20/Poly1305/NoPadding", + "XChaCha20-Poly1305", + "xChaCha20/poly1305/nopadding", + // Adding Mode and padding to alias also works. + "XChaCha20-Poly1305/Poly1305/NoPadding", + "xchacha20-poly1305/poly1305/nopadding", + }) { + assertDecryptsToExpectedPlaintext(transformation, "XChaCha20", key32, iv, + ciphertextXChaCha20Poly1305, plaintextVar); + assertDecryptsToExpectedPlaintext(transformation, "xchacha20", key32, iv, + ciphertextXChaCha20Poly1305, plaintextVar); + } + } + + // ========================================================================= + // 2. Unsupported Key Size Rejection Tests + // ========================================================================= + + @Test + public void initWithWrongKeySize_aes128EcbNoPadding_throws() throws Exception { + assertRejectsKey("AES_128/ECB/NoPadding", "AES", key24, null); + assertRejectsKey("AES_128/ECB/NoPadding", "AES", key32, null); + } + + @Test + public void initWithWrongKeySize_aes128EcbPkcs5Padding_andAliases_throws() throws Exception { + for (String transformation : + new String[] {"AES_128/ECB/PKCS5Padding", "AES_128/ECB/PKCS7Padding"}) { + assertRejectsKey(transformation, "AES", key24, null); + assertRejectsKey(transformation, "AES", key32, null); + } + } + + @Test + public void initWithWrongKeySize_aes128CbcNoPadding_throws() throws Exception { + IvParameterSpec iv = new IvParameterSpec(iv16); + assertRejectsKey("AES_128/CBC/NoPadding", "AES", key24, iv); + assertRejectsKey("AES_128/CBC/NoPadding", "AES", key32, iv); + } + + @Test + public void initWithWrongKeySize_aes128CbcPkcs5Padding_andAliases_throws() throws Exception { + IvParameterSpec iv = new IvParameterSpec(iv16); + String[] transformations = { + "AES_128/CBC/PKCS5Padding", "AES_128/CBC/PKCS7Padding", + "PBEWithHmacSHA1AndAES_128", "PBEWithHmacSHA224AndAES_128", + "PBEWithHmacSHA256AndAES_128", "PBEWithHmacSHA384AndAES_128", + "PBEWithHmacSHA512AndAES_128", + }; + for (String transformation : transformations) { + assertRejectsKey(transformation, "AES", key24, iv); + assertRejectsKey(transformation, "AES", key32, iv); + } + } + + @Test + public void initWithWrongKeySize_aes256EcbNoPadding_throws() throws Exception { + assertRejectsKey("AES_256/ECB/NoPadding", "AES", key16, null); + assertRejectsKey("AES_256/ECB/NoPadding", "AES", key24, null); + } + + @Test + public void initWithWrongKeySize_aes256EcbPkcs5Padding_andAliases_throws() throws Exception { + for (String transformation : + new String[] {"AES_256/ECB/PKCS5Padding", "AES_256/ECB/PKCS7Padding"}) { + assertRejectsKey(transformation, "AES", key16, null); + assertRejectsKey(transformation, "AES", key24, null); + } + } + + @Test + public void initWithWrongKeySize_aes256CbcNoPadding_throws() throws Exception { + IvParameterSpec iv = new IvParameterSpec(iv16); + assertRejectsKey("AES_256/CBC/NoPadding", "AES", key16, iv); + assertRejectsKey("AES_256/CBC/NoPadding", "AES", key24, iv); + } + + @Test + public void initWithWrongKeySize_aes256CbcPkcs5Padding_andAliases_throws() throws Exception { + IvParameterSpec iv = new IvParameterSpec(iv16); + String[] transformations = { + "AES_256/CBC/PKCS5Padding", "AES_256/CBC/PKCS7Padding", + "PBEWithHmacSHA1AndAES_256", "PBEWithHmacSHA224AndAES_256", + "PBEWithHmacSHA256AndAES_256", "PBEWithHmacSHA384AndAES_256", + "PBEWithHmacSHA512AndAES_256", + }; + for (String transformation : transformations) { + assertRejectsKey(transformation, "AES", key16, iv); + assertRejectsKey(transformation, "AES", key24, iv); + } + } + + @Test + public void initWith24ByteKey_aesGcmNoPadding_andAliases_throws() throws Exception { + GCMParameterSpec spec = new GCMParameterSpec(128, iv12); + String[] transformations = { + "AES/GCM/NoPadding", "GCM", + "2.16.840.1.101.3.4.1.6", "2.16.840.1.101.3.4.1.26", + "2.16.840.1.101.3.4.1.46", + }; + for (String transformation : transformations) { + assertRejectsKey(transformation, "AES", key24, spec); + } + } + + @Test + public void initWithWrongKeySize_aes128GcmNoPadding_throws() throws Exception { + GCMParameterSpec spec = new GCMParameterSpec(128, iv12); + assertRejectsKey("AES_128/GCM/NoPadding", "AES", key24, spec); + assertRejectsKey("AES_128/GCM/NoPadding", "AES", key32, spec); + } + + @Test + public void initWithWrongKeySize_aes256GcmNoPadding_throws() throws Exception { + GCMParameterSpec spec = new GCMParameterSpec(128, iv12); + assertRejectsKey("AES_256/GCM/NoPadding", "AES", key16, spec); + assertRejectsKey("AES_256/GCM/NoPadding", "AES", key24, spec); + } + + @Test + public void initWith24ByteKey_aesGcmSivNoPadding_throws() throws Exception { + assertRejectsKey("AES/GCM-SIV/NoPadding", "AES", key24, new GCMParameterSpec(128, iv12)); + } + + @Test + public void initWithWrongKeySize_aes128GcmSivNoPadding_throws() throws Exception { + GCMParameterSpec spec = new GCMParameterSpec(128, iv12); + assertRejectsKey("AES_128/GCM-SIV/NoPadding", "AES", key24, spec); + assertRejectsKey("AES_128/GCM-SIV/NoPadding", "AES", key32, spec); + } + + @Test + public void initWithWrongKeySize_aes256GcmSivNoPadding_throws() throws Exception { + GCMParameterSpec spec = new GCMParameterSpec(128, iv12); + assertRejectsKey("AES_256/GCM-SIV/NoPadding", "AES", key16, spec); + assertRejectsKey("AES_256/GCM-SIV/NoPadding", "AES", key24, spec); + } + + @Test + public void initWith24ByteKey_chaCha20_throws() throws Exception { + assertRejectsKey("ChaCha20", "ChaCha20", key24, new IvParameterSpec(iv12)); + } + + @Test + public void initWith24ByteKey_chaCha20Poly1305_andAliases_throws() throws Exception { + IvParameterSpec iv = new IvParameterSpec(iv12); + for (String transformation : + new String[] {"ChaCha20/Poly1305/NoPadding", "ChaCha20-Poly1305"}) { + assertRejectsKey(transformation, "ChaCha20", key24, iv); + } + } + + @Test + public void initWith24ByteKey_xChaCha20Poly1305_andAliases_throws() throws Exception { + IvParameterSpec iv = new IvParameterSpec(iv24); + for (String transformation : + new String[] {"XChaCha20/Poly1305/NoPadding", "XChaCha20-Poly1305"}) { + assertRejectsKey(transformation, "XChaCha20", key24, iv); + } + } +} diff --git a/platform/src/main/java/org/conscrypt/OpenSslHkdf.java b/platform/src/main/java/org/conscrypt/OpenSslHkdf.java new file mode 100644 index 000000000..3e26c26b9 --- /dev/null +++ b/platform/src/main/java/org/conscrypt/OpenSslHkdf.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * 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 org.conscrypt; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.AlgorithmParameterSpec; +import java.util.List; +import java.util.Objects; + +import javax.crypto.KDFParameters; +import javax.crypto.KDFSpi; +import javax.crypto.SecretKey; +import javax.crypto.spec.HKDFParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * OpenSSL Key Derivation Function (KDF) implementation supporting HKDF algorithms per RFC 5869. + */ +@android.annotation.Hide +@SuppressWarnings("NewApi") +public abstract class OpenSslHkdf extends KDFSpi { + private final Hkdf hkdf; + + protected OpenSslHkdf(String hmacAlgorithm, KDFParameters kdfParameters) + throws InvalidAlgorithmParameterException { + super(kdfParameters); + if (kdfParameters != null) { + throw new InvalidAlgorithmParameterException("KDFParameters are not supported"); + } + Objects.requireNonNull(hmacAlgorithm, "hmacAlgorithm == null"); + try { + this.hkdf = new Hkdf(hmacAlgorithm); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError("Standard HMAC algorithm unavailable: " + hmacAlgorithm, e); + } + } + + @Override + public KDFParameters engineGetParameters() { + return null; + } + + @Override + public SecretKey engineDeriveKey(String alg, AlgorithmParameterSpec derivationSpec) + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException { + Objects.requireNonNull(alg, "alg == null"); + if (alg.isEmpty()) { + throw new NoSuchAlgorithmException("Algorithm name cannot be empty"); + } + byte[] keyBytes = engineDeriveData(derivationSpec); + return new SecretKeySpec(keyBytes, alg); + } + + @Override + public byte[] engineDeriveData(AlgorithmParameterSpec derivationSpec) + throws InvalidAlgorithmParameterException { + Objects.requireNonNull(derivationSpec, "derivationSpec == null"); + if (derivationSpec instanceof HKDFParameterSpec.Extract) { + return deriveExtract((HKDFParameterSpec.Extract) derivationSpec); + } else if (derivationSpec instanceof HKDFParameterSpec.Expand) { + return deriveExpand((HKDFParameterSpec.Expand) derivationSpec); + } else if (derivationSpec instanceof HKDFParameterSpec.ExtractThenExpand) { + return deriveExtractThenExpand((HKDFParameterSpec.ExtractThenExpand) derivationSpec); + } else { + throw new InvalidAlgorithmParameterException("Unsupported derivationSpec: " + + derivationSpec.getClass().getName()); + } + } + + private byte[] deriveExtract(HKDFParameterSpec.Extract extract) + throws InvalidAlgorithmParameterException { + byte[] ikm = concatKeys(extract.ikms(), "IKM"); + if (ikm.length == 0) { + throw new InvalidAlgorithmParameterException( + "Input keying material (IKM) must not be empty"); + } + byte[] salt = concatKeys(extract.salts(), "salt"); + try { + return hkdf.extract(salt, ikm); + } catch (InvalidKeyException | IllegalArgumentException e) { + throw new InvalidAlgorithmParameterException("HKDF extract failed", e); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError("Standard HMAC algorithm unavailable", e); + } + } + + private byte[] deriveExpand(HKDFParameterSpec.Expand expand) + throws InvalidAlgorithmParameterException { + SecretKey prkKey = expand.prk(); + if (prkKey == null) { + throw new InvalidAlgorithmParameterException("PRK must not be null"); + } + if (!"RAW".equalsIgnoreCase(prkKey.getFormat())) { + throw new InvalidAlgorithmParameterException("PRK must be in RAW format, but was: " + + prkKey.getFormat()); + } + byte[] prk = prkKey.getEncoded(); + if (prk == null) { + throw new InvalidAlgorithmParameterException("PRK is not extractable"); + } + if (prk.length < hkdf.getMacLength()) { + throw new InvalidAlgorithmParameterException("PRK length (" + prk.length + + ") is less than HashLen (" + + hkdf.getMacLength() + ")"); + } + byte[] info = expand.info(); + if (info == null) { + info = new byte[0]; + } + int length = expand.length(); + try { + return hkdf.expand(prk, info, length); + } catch (InvalidKeyException | IllegalArgumentException e) { + throw new InvalidAlgorithmParameterException("HKDF expand failed", e); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError("Standard HMAC algorithm unavailable", e); + } + } + + private byte[] deriveExtractThenExpand(HKDFParameterSpec.ExtractThenExpand extractThenExpand) + throws InvalidAlgorithmParameterException { + byte[] ikm = concatKeys(extractThenExpand.ikms(), "IKM"); + if (ikm.length == 0) { + throw new InvalidAlgorithmParameterException( + "Input keying material (IKM) must not be empty"); + } + byte[] salt = concatKeys(extractThenExpand.salts(), "salt"); + byte[] info = extractThenExpand.info(); + if (info == null) { + info = new byte[0]; + } + int length = extractThenExpand.length(); + try { + byte[] prk = hkdf.extract(salt, ikm); + return hkdf.expand(prk, info, length); + } catch (InvalidKeyException | IllegalArgumentException e) { + throw new InvalidAlgorithmParameterException("HKDF extract-then-expand failed", e); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError("Standard HMAC algorithm unavailable", e); + } + } + + private static byte[] concatKeys(List keys, String name) + throws InvalidAlgorithmParameterException { + if (keys == null || keys.isEmpty()) { + return new byte[0]; + } + int totalLen = 0; + byte[][] encodedKeys = new byte[keys.size()][]; + for (int i = 0; i < keys.size(); i++) { + SecretKey key = keys.get(i); + if (key == null) { + throw new InvalidAlgorithmParameterException("Null " + name + " in key list"); + } + if (!"RAW".equalsIgnoreCase(key.getFormat())) { + throw new InvalidAlgorithmParameterException( + name + " key must be in RAW format, but was: " + key.getFormat()); + } + byte[] encoded = key.getEncoded(); + if (encoded == null) { + throw new InvalidAlgorithmParameterException( + name + " key does not provide encoded bytes (unextractable)"); + } + encodedKeys[i] = encoded; + totalLen += encoded.length; + } + if (keys.size() == 1) { + return encodedKeys[0]; + } + byte[] result = new byte[totalLen]; + int pos = 0; + for (byte[] encoded : encodedKeys) { + System.arraycopy(encoded, 0, result, pos, encoded.length); + pos += encoded.length; + } + if (pos != totalLen) { + throw new IllegalArgumentException("Unexpected concatenated key length"); + } + return result; + } + + public static final class HkdfSha256 extends OpenSslHkdf { + public HkdfSha256(KDFParameters kdfParameters) throws InvalidAlgorithmParameterException { + super("HmacSHA256", kdfParameters); + } + } + + public static final class HkdfSha384 extends OpenSslHkdf { + public HkdfSha384(KDFParameters kdfParameters) throws InvalidAlgorithmParameterException { + super("HmacSHA384", kdfParameters); + } + } + + public static final class HkdfSha512 extends OpenSslHkdf { + public HkdfSha512(KDFParameters kdfParameters) throws InvalidAlgorithmParameterException { + super("HmacSHA512", kdfParameters); + } + } + + public static final class HkdfSha1 extends OpenSslHkdf { + public HkdfSha1(KDFParameters kdfParameters) throws InvalidAlgorithmParameterException { + super("HmacSHA1", kdfParameters); + } + } +} diff --git a/platform/src/main/java/org/conscrypt/OpenSslMlKem.java b/platform/src/main/java/org/conscrypt/OpenSslMlKem.java new file mode 100644 index 000000000..31f4f4736 --- /dev/null +++ b/platform/src/main/java/org/conscrypt/OpenSslMlKem.java @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * 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 org.conscrypt; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; +import java.util.Objects; + +import javax.crypto.DecapsulateException; +import javax.crypto.KEM; +import javax.crypto.KEMSpi; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +/** + * OpenSSL Key Encapsulation Mechanism (KEM) SPI implementation for ML-KEM algorithms. + */ +@android.annotation.Hide // This class is not part of the Android public SDK API +@SuppressWarnings("NewApi") +@Internal +public final class OpenSslMlKem { + private OpenSslMlKem() {} + + /** Generic ML-KEM implementation supporting all ML-KEM variants. */ + public static class MlKem implements KEMSpi { + public MlKem() {} + + @Override + public EncapsulatorSpi engineNewEncapsulator(PublicKey publicKey, + AlgorithmParameterSpec spec, + SecureRandom secureRandom) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if (spec != null) { + throw new InvalidAlgorithmParameterException( + "AlgorithmParameterSpec not supported for ML-KEM"); + } + OpenSslMlKemPublicKey key = + (OpenSslMlKemPublicKey) new OpenSslMlKemKeyFactory.MlKem().engineTranslateKey( + publicKey); + if (key.getMlKemAlgorithm().equals(MlKemAlgorithm.ML_KEM_768)) { + return new MlKem768Encapsulator(key.getRaw()); + } else if (key.getMlKemAlgorithm().equals(MlKemAlgorithm.ML_KEM_1024)) { + return new MlKem1024Encapsulator(key.getRaw()); + } + throw new InvalidKeyException("Unsupported ML-KEM key variant: " + + key.getMlKemAlgorithm()); + } + + @Override + public DecapsulatorSpi engineNewDecapsulator(PrivateKey privateKey, + AlgorithmParameterSpec spec) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if (spec != null) { + throw new InvalidAlgorithmParameterException( + "AlgorithmParameterSpec not supported for ML-KEM"); + } + OpenSslMlKemPrivateKey key = + (OpenSslMlKemPrivateKey) new OpenSslMlKemKeyFactory.MlKem().engineTranslateKey( + privateKey); + if (key.getMlKemAlgorithm().equals(MlKemAlgorithm.ML_KEM_768)) { + return new MlKem768Decapsulator(key.getSeed()); + } else if (key.getMlKemAlgorithm().equals(MlKemAlgorithm.ML_KEM_1024)) { + return new MlKem1024Decapsulator(key.getSeed()); + } + throw new InvalidKeyException("Unsupported ML-KEM key variant: " + + key.getMlKemAlgorithm()); + } + } + + /** ML-KEM-768 implementation. */ + public static class MlKem768 implements KEMSpi { + static final int CIPHERTEXT_SIZE = 1088; + static final int SECRET_SIZE = 32; + + public MlKem768() {} + + @Override + public EncapsulatorSpi engineNewEncapsulator(PublicKey publicKey, + AlgorithmParameterSpec spec, + SecureRandom secureRandom) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if (spec != null) { + throw new InvalidAlgorithmParameterException( + "AlgorithmParameterSpec not supported for ML-KEM-768"); + } + OpenSslMlKemPublicKey key = + (OpenSslMlKemPublicKey) new OpenSslMlKemKeyFactory.MlKem768() + .engineTranslateKey(publicKey); + return new MlKem768Encapsulator(key.getRaw()); + } + + @Override + public DecapsulatorSpi engineNewDecapsulator(PrivateKey privateKey, + AlgorithmParameterSpec spec) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if (spec != null) { + throw new InvalidAlgorithmParameterException( + "AlgorithmParameterSpec not supported for ML-KEM-768"); + } + OpenSslMlKemPrivateKey key = + (OpenSslMlKemPrivateKey) new OpenSslMlKemKeyFactory.MlKem768() + .engineTranslateKey(privateKey); + return new MlKem768Decapsulator(key.getSeed()); + } + } + + /** ML-KEM-1024 implementation. */ + public static class MlKem1024 implements KEMSpi { + static final int CIPHERTEXT_SIZE = 1568; + static final int SECRET_SIZE = 32; + + public MlKem1024() {} + + @Override + public EncapsulatorSpi engineNewEncapsulator(PublicKey publicKey, + AlgorithmParameterSpec spec, + SecureRandom secureRandom) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if (spec != null) { + throw new InvalidAlgorithmParameterException( + "AlgorithmParameterSpec not supported for ML-KEM-1024"); + } + OpenSslMlKemPublicKey key = + (OpenSslMlKemPublicKey) new OpenSslMlKemKeyFactory.MlKem1024() + .engineTranslateKey(publicKey); + return new MlKem1024Encapsulator(key.getRaw()); + } + + @Override + public DecapsulatorSpi engineNewDecapsulator(PrivateKey privateKey, + AlgorithmParameterSpec spec) + throws InvalidAlgorithmParameterException, InvalidKeyException { + if (spec != null) { + throw new InvalidAlgorithmParameterException( + "AlgorithmParameterSpec not supported for ML-KEM-1024"); + } + OpenSslMlKemPrivateKey key = + (OpenSslMlKemPrivateKey) new OpenSslMlKemKeyFactory.MlKem1024() + .engineTranslateKey(privateKey); + return new MlKem1024Decapsulator(key.getSeed()); + } + } + + private static final class MlKem768Encapsulator implements KEMSpi.EncapsulatorSpi { + private final byte[] rawPublicKey; + + MlKem768Encapsulator(byte[] rawPublicKey) { + this.rawPublicKey = rawPublicKey; + } + + @Override + public KEM.Encapsulated engineEncapsulate(int from, int to, String algorithm) { + Objects.requireNonNull(algorithm, "algorithm == null"); + checkKeyRange(from, to, MlKem768.SECRET_SIZE); + + byte[] outCiphertext = new byte[MlKem768.CIPHERTEXT_SIZE]; + byte[] outSharedSecret = new byte[MlKem768.SECRET_SIZE]; + NativeCrypto.MLKEM768_encap(rawPublicKey, outCiphertext, outSharedSecret); + + byte[] keyBytes = Arrays.copyOfRange(outSharedSecret, from, to); + SecretKey secretKey = new SecretKeySpec(keyBytes, algorithm); + return new KEM.Encapsulated(secretKey, outCiphertext, null); + } + + @Override + public int engineSecretSize() { + return MlKem768.SECRET_SIZE; + } + + @Override + public int engineEncapsulationSize() { + return MlKem768.CIPHERTEXT_SIZE; + } + } + + private static final class MlKem768Decapsulator implements KEMSpi.DecapsulatorSpi { + private final byte[] seed; + + MlKem768Decapsulator(byte[] seed) { + this.seed = seed; + } + + @Override + public SecretKey engineDecapsulate(byte[] encapsulation, int from, int to, String algorithm) + throws DecapsulateException { + Objects.requireNonNull(encapsulation, "encapsulation == null"); + Objects.requireNonNull(algorithm, "algorithm == null"); + if (encapsulation.length != MlKem768.CIPHERTEXT_SIZE) { + throw new DecapsulateException("Invalid encapsulation size: " + encapsulation.length + + ", expected: " + MlKem768.CIPHERTEXT_SIZE); + } + checkKeyRange(from, to, MlKem768.SECRET_SIZE); + + byte[] outSharedSecret; + try { + outSharedSecret = NativeCrypto.MLKEM768_decap(encapsulation, seed); + } catch (Exception e) { + throw new DecapsulateException("Failed to decapsulate ML-KEM-768", e); + } + if (outSharedSecret == null || outSharedSecret.length != MlKem768.SECRET_SIZE) { + throw new DecapsulateException("Decapsulation produced invalid shared secret"); + } + + byte[] keyBytes = Arrays.copyOfRange(outSharedSecret, from, to); + return new SecretKeySpec(keyBytes, algorithm); + } + + @Override + public int engineSecretSize() { + return MlKem768.SECRET_SIZE; + } + + @Override + public int engineEncapsulationSize() { + return MlKem768.CIPHERTEXT_SIZE; + } + } + + private static final class MlKem1024Encapsulator implements KEMSpi.EncapsulatorSpi { + private final byte[] rawPublicKey; + + MlKem1024Encapsulator(byte[] rawPublicKey) { + this.rawPublicKey = rawPublicKey; + } + + @Override + public KEM.Encapsulated engineEncapsulate(int from, int to, String algorithm) { + Objects.requireNonNull(algorithm, "algorithm == null"); + checkKeyRange(from, to, MlKem1024.SECRET_SIZE); + + byte[] outCiphertext = new byte[MlKem1024.CIPHERTEXT_SIZE]; + byte[] outSharedSecret = new byte[MlKem1024.SECRET_SIZE]; + NativeCrypto.MLKEM1024_encap(rawPublicKey, outCiphertext, outSharedSecret); + + byte[] keyBytes = Arrays.copyOfRange(outSharedSecret, from, to); + SecretKey secretKey = new SecretKeySpec(keyBytes, algorithm); + return new KEM.Encapsulated(secretKey, outCiphertext, null); + } + + @Override + public int engineSecretSize() { + return MlKem1024.SECRET_SIZE; + } + + @Override + public int engineEncapsulationSize() { + return MlKem1024.CIPHERTEXT_SIZE; + } + } + + private static final class MlKem1024Decapsulator implements KEMSpi.DecapsulatorSpi { + private final byte[] seed; + + MlKem1024Decapsulator(byte[] seed) { + this.seed = seed; + } + + @Override + public SecretKey engineDecapsulate(byte[] encapsulation, int from, int to, String algorithm) + throws DecapsulateException { + Objects.requireNonNull(encapsulation, "encapsulation == null"); + Objects.requireNonNull(algorithm, "algorithm == null"); + if (encapsulation.length != MlKem1024.CIPHERTEXT_SIZE) { + throw new DecapsulateException("Invalid encapsulation size: " + encapsulation.length + + ", expected: " + MlKem1024.CIPHERTEXT_SIZE); + } + checkKeyRange(from, to, MlKem1024.SECRET_SIZE); + + byte[] outSharedSecret; + try { + outSharedSecret = NativeCrypto.MLKEM1024_decap(encapsulation, seed); + } catch (Exception e) { + throw new DecapsulateException("Failed to decapsulate ML-KEM-1024", e); + } + if (outSharedSecret == null || outSharedSecret.length != MlKem1024.SECRET_SIZE) { + throw new DecapsulateException("Decapsulation produced invalid shared secret"); + } + + byte[] keyBytes = Arrays.copyOfRange(outSharedSecret, from, to); + return new SecretKeySpec(keyBytes, algorithm); + } + + @Override + public int engineSecretSize() { + return MlKem1024.SECRET_SIZE; + } + + @Override + public int engineEncapsulationSize() { + return MlKem1024.CIPHERTEXT_SIZE; + } + } + + private static void checkKeyRange(int from, int to, int secretSize) { + if (from < 0 || from > to || to > secretSize) { + throw new IndexOutOfBoundsException("Invalid range: from=" + from + ", to=" + to + + ", secretSize=" + secretSize); + } + } +} diff --git a/platform/src/main/java/org/conscrypt/Platform.java b/platform/src/main/java/org/conscrypt/Platform.java index ee9fec5f6..a755cedc3 100644 --- a/platform/src/main/java/org/conscrypt/Platform.java +++ b/platform/src/main/java/org/conscrypt/Platform.java @@ -19,6 +19,8 @@ import static android.system.OsConstants.SOL_SOCKET; import static android.system.OsConstants.SO_SNDTIMEO; +import android.compat.Compatibility; +import android.compat.annotation.ChangeId; import android.system.ErrnoException; import android.system.Os; import android.system.StructTimeval; @@ -644,6 +646,38 @@ public static boolean isPakeSupported() { return true; } + /** + * Controls whether Post-Quantum Cryptography (X25519MLKEM768) is included in the default + * TLS named groups for this application. + */ + @ChangeId + public static final long ENABLE_TLS_PQC_DEFAULT_GROUPS = 556729260L; + + /** + * Returns true if PQC key exchange (X25519MLKEM768) should be enabled by default. + * + *

Note: Do not cache the result of {@link Compatibility#isChangeEnabled(long)} in a static + * field. Conscrypt classes are preloaded in Zygote ({@link #RUNNING_IN_ZYGOTE}), where + * {@code Compatibility.isChangeEnabled} always returns true. This check must be evaluated + * dynamically per connection post-fork inside the application process. + * + *

Unlike {@code @EnabledAfter}/{@code @EnabledSince} targetSdk-gated changes, + * {@link #ENABLE_TLS_PQC_DEFAULT_GROUPS} is enabled by default across all targetSdkVersions, so + * {@code PlatformCompat} evaluates it to {@code true} for both app and non-app processes on + * older Android releases as well, while still allowing per-app overrides on debuggable builds + * and via Developer Options. + */ + public static boolean isPqcDefaultEnabled() { + if (!Flags.tlsPqcDefaultNamedGroups()) { + return false; + } + try { + return Compatibility.isChangeEnabled(ENABLE_TLS_PQC_DEFAULT_GROUPS); + } catch (Throwable ignored) { + return true; + } + } + private static boolean inZygote() { if (canCallZygoteMethod) { return ZygoteHooks.isInZygote(); diff --git a/platform/src/test/java/org/conscrypt/OpenSslHkdfTest.java b/platform/src/test/java/org/conscrypt/OpenSslHkdfTest.java new file mode 100644 index 000000000..5e6200f67 --- /dev/null +++ b/platform/src/test/java/org/conscrypt/OpenSslHkdfTest.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * 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 org.conscrypt; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.spec.AlgorithmParameterSpec; + +import javax.crypto.KDF; +import javax.crypto.KDFParameters; +import javax.crypto.SecretKey; +import javax.crypto.spec.HKDFParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +@SuppressWarnings("NewApi") +@RunWith(JUnit4.class) +public class OpenSslHkdfTest { + // RFC 5869 Test Case 1 (SHA-256) + private static final byte[] RFC5869_TC1_IKM = + TestUtils.decodeHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); + private static final byte[] RFC5869_TC1_SALT = + TestUtils.decodeHex("000102030405060708090a0b0c"); + private static final byte[] RFC5869_TC1_INFO = TestUtils.decodeHex("f0f1f2f3f4f5f6f7f8f9"); + private static final int RFC5869_TC1_L = 42; + private static final byte[] RFC5869_TC1_PRK = + TestUtils.decodeHex("077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5"); + private static final byte[] RFC5869_TC1_OKM = TestUtils.decodeHex( + "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"); + + // RFC 5869 Test Case 2 (SHA-256 with long inputs) + private static final byte[] RFC5869_TC2_IKM = + TestUtils.decodeHex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + + "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f" + + "404142434445464748494a4b4c4d4e4f"); + private static final byte[] RFC5869_TC2_SALT = + TestUtils.decodeHex("606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f" + + "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f" + + "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"); + private static final byte[] RFC5869_TC2_INFO = + TestUtils.decodeHex("b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf" + + "d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef" + + "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"); + private static final int RFC5869_TC2_L = 82; + private static final byte[] RFC5869_TC2_PRK = + TestUtils.decodeHex("06a6b88c5853361a06104c9ceb35b45cef760014904671014a193f40c15fc244"); + private static final byte[] RFC5869_TC2_OKM = + TestUtils.decodeHex("b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c" + + "59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71" + + "cc30c58179ec3e87c14c01d5c1f3434f1d87"); + + // RFC 5869 Test Case 3 (SHA-256 zero-length salt/info) + private static final byte[] RFC5869_TC3_IKM = + TestUtils.decodeHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); + private static final int RFC5869_TC3_L = 42; + private static final byte[] RFC5869_TC3_PRK = + TestUtils.decodeHex("19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04"); + private static final byte[] RFC5869_TC3_OKM = + TestUtils.decodeHex("8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d" + + "9d201395faa4b61a96c8"); + + @Test + public void testParametersNotSupported() { + assertThrows(InvalidAlgorithmParameterException.class, + () -> new OpenSslHkdf.HkdfSha256(new KDFParameters() {})); + } + + @Test + public void testEngineGetParameters() throws Exception { + OpenSslHkdf kdf = new OpenSslHkdf.HkdfSha256(null); + assertNull(kdf.engineGetParameters()); + } + + @Test + public void testRfc5869TestCase1_Extract() throws Exception { + OpenSslHkdf kdf = new OpenSslHkdf.HkdfSha256(null); + HKDFParameterSpec spec = HKDFParameterSpec.ofExtract() + .addIKM(RFC5869_TC1_IKM) + .addSalt(RFC5869_TC1_SALT) + .extractOnly(); + + byte[] prk = kdf.engineDeriveData(spec); + assertArrayEquals(RFC5869_TC1_PRK, prk); + } + + @Test + public void testRfc5869TestCase1_Expand() throws Exception { + OpenSslHkdf kdf = new OpenSslHkdf.HkdfSha256(null); + SecretKey prkKey = new SecretKeySpec(RFC5869_TC1_PRK, "Generic"); + HKDFParameterSpec spec = + HKDFParameterSpec.expandOnly(prkKey, RFC5869_TC1_INFO, RFC5869_TC1_L); + + byte[] okm = kdf.engineDeriveData(spec); + assertArrayEquals(RFC5869_TC1_OKM, okm); + } + + @Test + public void testRfc5869TestCase1_ExtractThenExpand() throws Exception { + OpenSslHkdf kdf = new OpenSslHkdf.HkdfSha256(null); + HKDFParameterSpec spec = HKDFParameterSpec.ofExtract() + .addIKM(RFC5869_TC1_IKM) + .addSalt(RFC5869_TC1_SALT) + .thenExpand(RFC5869_TC1_INFO, RFC5869_TC1_L); + + byte[] okm = kdf.engineDeriveData(spec); + assertArrayEquals(RFC5869_TC1_OKM, okm); + + SecretKey derivedKey = kdf.engineDeriveKey("AES", spec); + assertEquals("AES", derivedKey.getAlgorithm()); + assertArrayEquals(RFC5869_TC1_OKM, derivedKey.getEncoded()); + } + + @Test + public void testRfc5869TestCase2_ExtractThenExpand() throws Exception { + OpenSslHkdf kdf = new OpenSslHkdf.HkdfSha256(null); + HKDFParameterSpec spec = HKDFParameterSpec.ofExtract() + .addIKM(RFC5869_TC2_IKM) + .addSalt(RFC5869_TC2_SALT) + .thenExpand(RFC5869_TC2_INFO, RFC5869_TC2_L); + + byte[] okm = kdf.engineDeriveData(spec); + assertArrayEquals(RFC5869_TC2_OKM, okm); + } + + @Test + public void testRfc5869TestCase3_NoSaltNoInfo() throws Exception { + OpenSslHkdf kdf = new OpenSslHkdf.HkdfSha256(null); + HKDFParameterSpec spec = HKDFParameterSpec.ofExtract() + .addIKM(RFC5869_TC3_IKM) + .thenExpand(null, RFC5869_TC3_L); + + byte[] okm = kdf.engineDeriveData(spec); + assertArrayEquals(RFC5869_TC3_OKM, okm); + } + + @Test + public void testMultipleIKMsAndSaltsConcatenation() throws Exception { + OpenSslHkdf kdf = new OpenSslHkdf.HkdfSha256(null); + + // Split RFC5869_TC1_IKM into 2 pieces: 10 bytes and 12 bytes + byte[] ikmPart1 = new byte[10]; + byte[] ikmPart2 = new byte[12]; + System.arraycopy(RFC5869_TC1_IKM, 0, ikmPart1, 0, 10); + System.arraycopy(RFC5869_TC1_IKM, 10, ikmPart2, 0, 12); + + // Split RFC5869_TC1_SALT into 2 pieces: 6 bytes and 7 bytes + byte[] saltPart1 = new byte[6]; + byte[] saltPart2 = new byte[7]; + System.arraycopy(RFC5869_TC1_SALT, 0, saltPart1, 0, 6); + System.arraycopy(RFC5869_TC1_SALT, 6, saltPart2, 0, 7); + + HKDFParameterSpec spec = HKDFParameterSpec.ofExtract() + .addIKM(ikmPart1) + .addIKM(ikmPart2) + .addSalt(saltPart1) + .addSalt(saltPart2) + .thenExpand(RFC5869_TC1_INFO, RFC5869_TC1_L); + + byte[] okm = kdf.engineDeriveData(spec); + assertArrayEquals(RFC5869_TC1_OKM, okm); + } + + @Test + public void testInvalidParameters() throws Exception { + OpenSslHkdf kdf = new OpenSslHkdf.HkdfSha256(null); + + // Null derivationSpec + assertThrows(NullPointerException.class, () -> kdf.engineDeriveData(null)); + + // Unsupported derivationSpec + assertThrows(InvalidAlgorithmParameterException.class, + () -> kdf.engineDeriveData(new AlgorithmParameterSpec() {})); + + // Empty IKM in Extract + HKDFParameterSpec emptyIkmExtract = HKDFParameterSpec.ofExtract().extractOnly(); + assertThrows(InvalidAlgorithmParameterException.class, + () -> kdf.engineDeriveData(emptyIkmExtract)); + + // Empty alg name in deriveKey + HKDFParameterSpec validSpec = HKDFParameterSpec.ofExtract() + .addIKM(RFC5869_TC1_IKM) + .thenExpand(RFC5869_TC1_INFO, RFC5869_TC1_L); + assertThrows(NoSuchAlgorithmException.class, () -> kdf.engineDeriveKey("", validSpec)); + assertThrows(NullPointerException.class, () -> kdf.engineDeriveKey(null, validSpec)); + + // Output length > 255 * HashLen (255 * 32 + 1 = 8161 for SHA-256) + HKDFParameterSpec tooLongExpand = HKDFParameterSpec.expandOnly( + new SecretKeySpec(RFC5869_TC1_PRK, "RAW"), RFC5869_TC1_INFO, 255 * 32 + 1); + assertThrows(InvalidAlgorithmParameterException.class, + () -> kdf.engineDeriveData(tooLongExpand)); + HKDFParameterSpec tooLongExtractThenExpand = + HKDFParameterSpec.ofExtract() + .addIKM(RFC5869_TC1_IKM) + .thenExpand(RFC5869_TC1_INFO, 255 * 32 + 1); + assertThrows(InvalidAlgorithmParameterException.class, + () -> kdf.engineDeriveData(tooLongExtractThenExpand)); + } + + @Test + public void testNonRawKeyFormatRejection() throws Exception { + OpenSslHkdf kdf = new OpenSslHkdf.HkdfSha256(null); + SecretKey nonRawKey = new SecretKey() { + @Override + public String getAlgorithm() { + return "Generic"; + } + @Override + public String getFormat() { + return "PKCS#8"; + } + @Override + public byte[] getEncoded() { + return new byte[32]; + } + }; + + HKDFParameterSpec specExpand = + HKDFParameterSpec.expandOnly(nonRawKey, RFC5869_TC1_INFO, 32); + assertThrows(InvalidAlgorithmParameterException.class, + () -> kdf.engineDeriveData(specExpand)); + + HKDFParameterSpec specExtract = + HKDFParameterSpec.ofExtract().addIKM(nonRawKey).extractOnly(); + assertThrows(InvalidAlgorithmParameterException.class, + () -> kdf.engineDeriveData(specExtract)); + } + + @Test + public void testHKDFSHA384AndSHA512() throws Exception { + OpenSslHkdf kdf384 = new OpenSslHkdf.HkdfSha384(null); + HKDFParameterSpec spec384 = HKDFParameterSpec.ofExtract() + .addIKM(RFC5869_TC1_IKM) + .thenExpand(RFC5869_TC1_INFO, 48); + byte[] okm384 = kdf384.engineDeriveData(spec384); + assertEquals(48, okm384.length); + + OpenSslHkdf kdf512 = new OpenSslHkdf.HkdfSha512(null); + HKDFParameterSpec spec512 = HKDFParameterSpec.ofExtract() + .addIKM(RFC5869_TC1_IKM) + .thenExpand(RFC5869_TC1_INFO, 64); + byte[] okm512 = kdf512.engineDeriveData(spec512); + assertEquals(64, okm512.length); + } + + @Test + public void testJceApi_Rfc5869AndAlgorithms() throws Exception { + if (!Platform.isPqcKemKdfSupported()) { + return; + } + Provider provider = TestUtils.getConscryptProvider(); + KDF kdf256 = KDF.getInstance("HKDF-SHA256", provider); + + // 1. Extract-only (RFC 5869 Test Case 1) + HKDFParameterSpec extractSpec = HKDFParameterSpec.ofExtract() + .addIKM(RFC5869_TC1_IKM) + .addSalt(RFC5869_TC1_SALT) + .extractOnly(); + assertArrayEquals(RFC5869_TC1_PRK, kdf256.deriveData(extractSpec)); + SecretKey prkKey = kdf256.deriveKey("Generic", extractSpec); + assertEquals("Generic", prkKey.getAlgorithm()); + assertArrayEquals(RFC5869_TC1_PRK, prkKey.getEncoded()); + + // 2. Expand-only (RFC 5869 Test Case 1) + HKDFParameterSpec expandSpec = + HKDFParameterSpec.expandOnly(prkKey, RFC5869_TC1_INFO, RFC5869_TC1_L); + assertArrayEquals(RFC5869_TC1_OKM, kdf256.deriveData(expandSpec)); + SecretKey expandedKey = kdf256.deriveKey("AES", expandSpec); + assertEquals("AES", expandedKey.getAlgorithm()); + assertArrayEquals(RFC5869_TC1_OKM, expandedKey.getEncoded()); + + // 3. Extract-then-expand (RFC 5869 Test Case 1 & Test Case 3 with no salt/info) + HKDFParameterSpec extractThenExpandSpec = + HKDFParameterSpec.ofExtract() + .addIKM(RFC5869_TC1_IKM) + .addSalt(RFC5869_TC1_SALT) + .thenExpand(RFC5869_TC1_INFO, RFC5869_TC1_L); + assertArrayEquals(RFC5869_TC1_OKM, kdf256.deriveData(extractThenExpandSpec)); + SecretKey derivedKey = kdf256.deriveKey("AES", extractThenExpandSpec); + assertEquals("AES", derivedKey.getAlgorithm()); + assertArrayEquals(RFC5869_TC1_OKM, derivedKey.getEncoded()); + + HKDFParameterSpec tc3Spec = HKDFParameterSpec.ofExtract() + .addIKM(RFC5869_TC3_IKM) + .thenExpand(null, RFC5869_TC3_L); + assertArrayEquals(RFC5869_TC3_OKM, kdf256.deriveData(tc3Spec)); + + // 4. Other HKDF hash variants (HKDF-SHA384, HKDF-SHA512, HKDF-SHA1) + assertEquals( + 48, + KDF.getInstance("HKDF-SHA384", provider) + .deriveData( + HKDFParameterSpec.ofExtract().addIKM(RFC5869_TC1_IKM).extractOnly()) + .length); + assertEquals( + 64, + KDF.getInstance("HKDF-SHA512", provider) + .deriveData( + HKDFParameterSpec.ofExtract().addIKM(RFC5869_TC1_IKM).extractOnly()) + .length); + assertEquals( + 20, + KDF.getInstance("HKDF-SHA1", provider) + .deriveData( + HKDFParameterSpec.ofExtract().addIKM(RFC5869_TC1_IKM).extractOnly()) + .length); + } +} diff --git a/platform/src/test/java/org/conscrypt/OpenSslMlKemTest.java b/platform/src/test/java/org/conscrypt/OpenSslMlKemTest.java new file mode 100644 index 000000000..05cac778b --- /dev/null +++ b/platform/src/test/java/org/conscrypt/OpenSslMlKemTest.java @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * 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 org.conscrypt; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import org.conscrypt.TestVector; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.List; + +import javax.crypto.DecapsulateException; +import javax.crypto.KEM; +import javax.crypto.KEMSpi; +import javax.crypto.SecretKey; + +@SuppressWarnings("NewApi") +@RunWith(JUnit4.class) +public class OpenSslMlKemTest { + @Test + public void mlKem768_encapsulateAndDecapsulate_matchesSharedSecret() throws Exception { + OpenSslMlKemKeyPairGenerator.MlKem768 kpg = new OpenSslMlKemKeyPairGenerator.MlKem768(); + KeyPair keyPair = kpg.generateKeyPair(); + + OpenSslMlKem.MlKem768 kem = new OpenSslMlKem.MlKem768(); + KEMSpi.EncapsulatorSpi encapsulator = + kem.engineNewEncapsulator(keyPair.getPublic(), null, null); + assertEquals(32, encapsulator.engineSecretSize()); + assertEquals(1088, encapsulator.engineEncapsulationSize()); + + KEM.Encapsulated encapsulated = encapsulator.engineEncapsulate(0, 32, "Generic"); + assertNotNull(encapsulated); + assertNotNull(encapsulated.key()); + assertEquals("Generic", encapsulated.key().getAlgorithm()); + assertEquals(32, encapsulated.key().getEncoded().length); + assertEquals(1088, encapsulated.encapsulation().length); + assertNull(encapsulated.params()); + + KEMSpi.DecapsulatorSpi decapsulator = kem.engineNewDecapsulator(keyPair.getPrivate(), null); + assertEquals(32, decapsulator.engineSecretSize()); + assertEquals(1088, decapsulator.engineEncapsulationSize()); + + SecretKey decapsulatedKey = + decapsulator.engineDecapsulate(encapsulated.encapsulation(), 0, 32, "Generic"); + assertNotNull(decapsulatedKey); + assertEquals("Generic", decapsulatedKey.getAlgorithm()); + assertArrayEquals(encapsulated.key().getEncoded(), decapsulatedKey.getEncoded()); + } + + @Test + public void mlKem768_encapsulateAndDecapsulate_withSlicingAndAlgorithm() throws Exception { + OpenSslMlKemKeyPairGenerator.MlKem768 kpg = new OpenSslMlKemKeyPairGenerator.MlKem768(); + KeyPair keyPair = kpg.generateKeyPair(); + + OpenSslMlKem.MlKem768 kem = new OpenSslMlKem.MlKem768(); + KEMSpi.EncapsulatorSpi encapsulator = + kem.engineNewEncapsulator(keyPair.getPublic(), null, null); + KEM.Encapsulated encapsulated = encapsulator.engineEncapsulate(0, 16, "AES"); + assertEquals("AES", encapsulated.key().getAlgorithm()); + assertEquals(16, encapsulated.key().getEncoded().length); + + KEMSpi.DecapsulatorSpi decapsulator = kem.engineNewDecapsulator(keyPair.getPrivate(), null); + SecretKey decapsulatedKey = + decapsulator.engineDecapsulate(encapsulated.encapsulation(), 0, 16, "AES"); + assertEquals("AES", decapsulatedKey.getAlgorithm()); + assertEquals(16, decapsulatedKey.getEncoded().length); + assertArrayEquals(encapsulated.key().getEncoded(), decapsulatedKey.getEncoded()); + } + + @Test + public void mlKem1024_encapsulateAndDecapsulate_matchesSharedSecret() throws Exception { + OpenSslMlKemKeyPairGenerator.MlKem1024 kpg = new OpenSslMlKemKeyPairGenerator.MlKem1024(); + KeyPair keyPair = kpg.generateKeyPair(); + + OpenSslMlKem.MlKem1024 kem = new OpenSslMlKem.MlKem1024(); + KEMSpi.EncapsulatorSpi encapsulator = + kem.engineNewEncapsulator(keyPair.getPublic(), null, null); + assertEquals(32, encapsulator.engineSecretSize()); + assertEquals(1568, encapsulator.engineEncapsulationSize()); + + KEM.Encapsulated encapsulated = encapsulator.engineEncapsulate(0, 32, "Generic"); + assertNotNull(encapsulated); + assertNotNull(encapsulated.key()); + assertEquals("Generic", encapsulated.key().getAlgorithm()); + assertEquals(32, encapsulated.key().getEncoded().length); + assertEquals(1568, encapsulated.encapsulation().length); + + KEMSpi.DecapsulatorSpi decapsulator = kem.engineNewDecapsulator(keyPair.getPrivate(), null); + assertEquals(32, decapsulator.engineSecretSize()); + assertEquals(1568, decapsulator.engineEncapsulationSize()); + + SecretKey decapsulatedKey = + decapsulator.engineDecapsulate(encapsulated.encapsulation(), 0, 32, "Generic"); + assertNotNull(decapsulatedKey); + assertEquals("Generic", decapsulatedKey.getAlgorithm()); + assertArrayEquals(encapsulated.key().getEncoded(), decapsulatedKey.getEncoded()); + } + + @Test + public void mlKem1024_encapsulateAndDecapsulate_withSlicingAndAlgorithm() throws Exception { + OpenSslMlKemKeyPairGenerator.MlKem1024 kpg = new OpenSslMlKemKeyPairGenerator.MlKem1024(); + KeyPair keyPair = kpg.generateKeyPair(); + + OpenSslMlKem.MlKem1024 kem = new OpenSslMlKem.MlKem1024(); + KEMSpi.EncapsulatorSpi encapsulator = + kem.engineNewEncapsulator(keyPair.getPublic(), null, null); + KEM.Encapsulated encapsulated = encapsulator.engineEncapsulate(8, 24, "HmacSHA256"); + assertEquals("HmacSHA256", encapsulated.key().getAlgorithm()); + assertEquals(16, encapsulated.key().getEncoded().length); + + KEMSpi.DecapsulatorSpi decapsulator = kem.engineNewDecapsulator(keyPair.getPrivate(), null); + SecretKey decapsulatedKey = + decapsulator.engineDecapsulate(encapsulated.encapsulation(), 8, 24, "HmacSHA256"); + assertEquals("HmacSHA256", decapsulatedKey.getAlgorithm()); + assertEquals(16, decapsulatedKey.getEncoded().length); + assertArrayEquals(encapsulated.key().getEncoded(), decapsulatedKey.getEncoded()); + } + + @Test + public void mlKem_generic_handlesBoth768And1024() throws Exception { + OpenSslMlKem.MlKem genericKem = new OpenSslMlKem.MlKem(); + + // ML-KEM-768 + OpenSslMlKemKeyPairGenerator.MlKem768 kpg768 = new OpenSslMlKemKeyPairGenerator.MlKem768(); + KeyPair kp768 = kpg768.generateKeyPair(); + + KEMSpi.EncapsulatorSpi enc768 = + genericKem.engineNewEncapsulator(kp768.getPublic(), null, null); + assertEquals(1088, enc768.engineEncapsulationSize()); + assertEquals(32, enc768.engineSecretSize()); + KEM.Encapsulated res768 = enc768.engineEncapsulate(0, 32, "AES"); + + KEMSpi.DecapsulatorSpi dec768 = genericKem.engineNewDecapsulator(kp768.getPrivate(), null); + assertEquals(1088, dec768.engineEncapsulationSize()); + SecretKey sec768 = dec768.engineDecapsulate(res768.encapsulation(), 0, 32, "AES"); + assertArrayEquals(res768.key().getEncoded(), sec768.getEncoded()); + + // ML-KEM-1024 + OpenSslMlKemKeyPairGenerator.MlKem1024 kpg1024 = + new OpenSslMlKemKeyPairGenerator.MlKem1024(); + KeyPair kp1024 = kpg1024.generateKeyPair(); + + KEMSpi.EncapsulatorSpi enc1024 = + genericKem.engineNewEncapsulator(kp1024.getPublic(), null, null); + assertEquals(1568, enc1024.engineEncapsulationSize()); + assertEquals(32, enc1024.engineSecretSize()); + KEM.Encapsulated res1024 = enc1024.engineEncapsulate(0, 32, "AES"); + + KEMSpi.DecapsulatorSpi dec1024 = + genericKem.engineNewDecapsulator(kp1024.getPrivate(), null); + assertEquals(1568, dec1024.engineEncapsulationSize()); + SecretKey sec1024 = dec1024.engineDecapsulate(res1024.encapsulation(), 0, 32, "AES"); + assertArrayEquals(res1024.key().getEncoded(), sec1024.getEncoded()); + } + + @Test + public void mlKem_testVectors_cryptoMlKemTxt() throws Exception { + List vectors = TestUtils.readTestVectors("crypto/mlkem.txt"); + + for (TestVector vector : vectors) { + String keyAlgorithm = vector.getString("key-algorithm"); + byte[] pk = vector.getBytes("pk"); + byte[] sk = vector.getBytes("sk"); + byte[] enc = vector.getBytes("enc"); + + MlKemAlgorithm algo = "ML-KEM-768".equals(keyAlgorithm) ? MlKemAlgorithm.ML_KEM_768 + : MlKemAlgorithm.ML_KEM_1024; + + PublicKey publicKey = new OpenSslMlKemPublicKey(pk, algo); + PrivateKey privateKey = new OpenSslMlKemPrivateKey(sk, algo); + + KEMSpi kem = algo.equals(MlKemAlgorithm.ML_KEM_768) ? new OpenSslMlKem.MlKem768() + : new OpenSslMlKem.MlKem1024(); + + // Decapsulate known test vector encapsulation + KEMSpi.DecapsulatorSpi decapsulator = kem.engineNewDecapsulator(privateKey, null); + SecretKey secretKey = decapsulator.engineDecapsulate(enc, 0, 32, "Generic"); + assertNotNull(secretKey); + assertEquals(32, secretKey.getEncoded().length); + + // Test roundtrip encapsulation & decapsulation + KEMSpi.EncapsulatorSpi encapsulator = kem.engineNewEncapsulator(publicKey, null, null); + KEM.Encapsulated newEncapsulated = encapsulator.engineEncapsulate(0, 32, "Generic"); + SecretKey roundtripKey = decapsulator.engineDecapsulate(newEncapsulated.encapsulation(), + 0, 32, "Generic"); + assertArrayEquals(newEncapsulated.key().getEncoded(), roundtripKey.getEncoded()); + + // Also test with generic MlKem + OpenSslMlKem.MlKem genericKem = new OpenSslMlKem.MlKem(); + KEMSpi.DecapsulatorSpi genericDecapsulator = + genericKem.engineNewDecapsulator(privateKey, null); + SecretKey genericSecretKey = + genericDecapsulator.engineDecapsulate(enc, 0, 32, "Generic"); + assertArrayEquals(secretKey.getEncoded(), genericSecretKey.getEncoded()); + + KEMSpi.EncapsulatorSpi genericEncapsulator = + genericKem.engineNewEncapsulator(publicKey, null, null); + KEM.Encapsulated genericEncapsulated = + genericEncapsulator.engineEncapsulate(0, 32, "Generic"); + SecretKey genericRoundtripKey = genericDecapsulator.engineDecapsulate( + genericEncapsulated.encapsulation(), 0, 32, "Generic"); + assertArrayEquals(genericEncapsulated.key().getEncoded(), + genericRoundtripKey.getEncoded()); + } + } + + @Test + public void newEncapsulator_invalidParameters_throwsExceptions() throws Exception { + OpenSslMlKemKeyPairGenerator.MlKem768 kpg768 = new OpenSslMlKemKeyPairGenerator.MlKem768(); + KeyPair keyPair768 = kpg768.generateKeyPair(); + OpenSslMlKemKeyPairGenerator.MlKem1024 kpg1024 = + new OpenSslMlKemKeyPairGenerator.MlKem1024(); + KeyPair keyPair1024 = kpg1024.generateKeyPair(); + + OpenSslMlKem.MlKem768 kem768 = new OpenSslMlKem.MlKem768(); + + // Null key + assertThrows(InvalidKeyException.class, + () -> kem768.engineNewEncapsulator(null, null, null)); + + // Unsupported spec + assertThrows( + InvalidAlgorithmParameterException.class, + () + -> kem768.engineNewEncapsulator(keyPair768.getPublic(), + new AlgorithmParameterSpec() {}, null)); + + // Mismatched algorithm key + assertThrows(InvalidKeyException.class, + () -> kem768.engineNewEncapsulator(keyPair1024.getPublic(), null, null)); + } + + @Test + public void newDecapsulator_invalidParameters_throwsExceptions() throws Exception { + OpenSslMlKemKeyPairGenerator.MlKem768 kpg768 = new OpenSslMlKemKeyPairGenerator.MlKem768(); + KeyPair keyPair768 = kpg768.generateKeyPair(); + OpenSslMlKemKeyPairGenerator.MlKem1024 kpg1024 = + new OpenSslMlKemKeyPairGenerator.MlKem1024(); + KeyPair keyPair1024 = kpg1024.generateKeyPair(); + + OpenSslMlKem.MlKem768 kem768 = new OpenSslMlKem.MlKem768(); + + // Null key + assertThrows(InvalidKeyException.class, () -> kem768.engineNewDecapsulator(null, null)); + + // Unsupported spec + assertThrows(InvalidAlgorithmParameterException.class, + () + -> kem768.engineNewDecapsulator(keyPair768.getPrivate(), + new AlgorithmParameterSpec() {})); + + // Mismatched algorithm key + assertThrows(InvalidKeyException.class, + () -> kem768.engineNewDecapsulator(keyPair1024.getPrivate(), null)); + } + + @Test + public void encapsulate_invalidRangeAndNullAlgorithm_throwsExceptions() throws Exception { + OpenSslMlKemKeyPairGenerator.MlKem768 kpg = new OpenSslMlKemKeyPairGenerator.MlKem768(); + KeyPair keyPair = kpg.generateKeyPair(); + + OpenSslMlKem.MlKem768 kem = new OpenSslMlKem.MlKem768(); + KEMSpi.EncapsulatorSpi encapsulator = + kem.engineNewEncapsulator(keyPair.getPublic(), null, null); + + // Null algorithm + assertThrows(NullPointerException.class, () -> encapsulator.engineEncapsulate(0, 32, null)); + + // Invalid range: from < 0 + assertThrows(IndexOutOfBoundsException.class, + () -> encapsulator.engineEncapsulate(-1, 32, "Generic")); + + // Invalid range: from > to + assertThrows(IndexOutOfBoundsException.class, + () -> encapsulator.engineEncapsulate(16, 8, "Generic")); + + // Invalid range: to > secretSize (32) + assertThrows(IndexOutOfBoundsException.class, + () -> encapsulator.engineEncapsulate(0, 33, "Generic")); + } + + @Test + public void decapsulate_invalidParameters_throwsExceptions() throws Exception { + OpenSslMlKemKeyPairGenerator.MlKem768 kpg = new OpenSslMlKemKeyPairGenerator.MlKem768(); + KeyPair keyPair = kpg.generateKeyPair(); + + OpenSslMlKem.MlKem768 kem = new OpenSslMlKem.MlKem768(); + KEMSpi.DecapsulatorSpi decapsulator = kem.engineNewDecapsulator(keyPair.getPrivate(), null); + + // Null encapsulation + assertThrows(NullPointerException.class, + () -> decapsulator.engineDecapsulate(null, 0, 32, "Generic")); + + // Null algorithm + assertThrows(NullPointerException.class, + () -> decapsulator.engineDecapsulate(new byte[1088], 0, 32, null)); + + // Invalid encapsulation length + assertThrows(DecapsulateException.class, + () -> decapsulator.engineDecapsulate(new byte[1087], 0, 32, "Generic")); + assertThrows(DecapsulateException.class, + () -> decapsulator.engineDecapsulate(new byte[1089], 0, 32, "Generic")); + + // Invalid range + assertThrows(IndexOutOfBoundsException.class, + () -> decapsulator.engineDecapsulate(new byte[1088], -1, 32, "Generic")); + assertThrows(IndexOutOfBoundsException.class, + () -> decapsulator.engineDecapsulate(new byte[1088], 16, 8, "Generic")); + assertThrows(IndexOutOfBoundsException.class, + () -> decapsulator.engineDecapsulate(new byte[1088], 0, 33, "Generic")); + } + + @Test + public void jceApi_encapsulateAndDecapsulate() throws Exception { + if (!Platform.isPqcKemKdfSupported()) { + return; + } + Provider provider = TestUtils.getConscryptProvider(); + for (String algorithm : new String[] {"ML-KEM", "ML-KEM-768", "ML-KEM-1024"}) { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(algorithm, provider); + KeyPair kp = kpg.generateKeyPair(); + + KeyFactory kf = KeyFactory.getInstance(algorithm, provider); + PublicKey pub = kf.generatePublic(new X509EncodedKeySpec(kp.getPublic().getEncoded())); + PrivateKey priv = + kf.generatePrivate(new PKCS8EncodedKeySpec(kp.getPrivate().getEncoded())); + + KEM kem = KEM.getInstance(algorithm, provider); + KEM.Encapsulator encapsulator = kem.newEncapsulator(pub); + KEM.Encapsulated encapsulated = encapsulator.encapsulate(0, 32, "AES"); + + KEM.Decapsulator decapsulator = kem.newDecapsulator(priv); + SecretKey decapsulated = + decapsulator.decapsulate(encapsulated.encapsulation(), 0, 32, "AES"); + assertEquals("AES", decapsulated.getAlgorithm()); + assertArrayEquals(encapsulated.key().getEncoded(), decapsulated.getEncoded()); + } + } +} diff --git a/platform/src/test/java/org/conscrypt/OpenSslProviderTest.java b/platform/src/test/java/org/conscrypt/OpenSslProviderTest.java new file mode 100644 index 000000000..30e9ddd57 --- /dev/null +++ b/platform/src/test/java/org/conscrypt/OpenSslProviderTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * 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 org.conscrypt; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Provider; + +import javax.crypto.KDF; +import javax.crypto.KEM; +import javax.crypto.SecretKey; +import javax.crypto.spec.HKDFParameterSpec; + +@android.annotation.Hide +@SuppressWarnings("NewApi") +@RunWith(JUnit4.class) +public class OpenSslProviderTest { + @Test + public void testProviderKdfAndKemRegistration() throws Exception { + Provider provider = TestUtils.getConscryptProvider(); + assertNotNull(provider); + + if (Platform.isPqcKemKdfSupported()) { + assertNotNull(provider.getService("KDF", "HKDF-SHA256")); + assertNotNull(provider.getService("KDF", "HKDF-SHA384")); + assertNotNull(provider.getService("KDF", "HKDF-SHA512")); + assertNotNull(provider.getService("KDF", "HKDF-SHA1")); + + assertNotNull(provider.getService("KEM", "ML-KEM-768")); + assertNotNull(provider.getService("KEM", "ML-KEM-1024")); + assertNotNull(provider.getService("KEM", "ML-KEM")); + + // End-to-end JCE KDF API test + for (String kdfAlg : + new String[] {"HKDF-SHA256", "HKDF-SHA384", "HKDF-SHA512", "HKDF-SHA1"}) { + KDF kdf = KDF.getInstance(kdfAlg, provider); + HKDFParameterSpec spec = HKDFParameterSpec.ofExtract() + .addIKM(new byte[] {1, 2, 3, 4}) + .addSalt(new byte[] {5, 6, 7, 8}) + .thenExpand(new byte[] {9, 10}, 32); + SecretKey derivedKey = kdf.deriveKey("AES", spec); + byte[] derivedData = kdf.deriveData(spec); + assertEquals("AES", derivedKey.getAlgorithm()); + assertEquals(32, derivedData.length); + assertArrayEquals(derivedData, derivedKey.getEncoded()); + } + + // End-to-end JCE KEM API test + for (String kemAlg : new String[] {"ML-KEM", "ML-KEM-768", "ML-KEM-1024"}) { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(kemAlg, provider); + KeyPair kp = kpg.generateKeyPair(); + + KEM kem = KEM.getInstance(kemAlg, provider); + KEM.Encapsulator encapsulator = kem.newEncapsulator(kp.getPublic()); + KEM.Encapsulated encapsulated = encapsulator.encapsulate(0, 32, "AES"); + + KEM.Decapsulator decapsulator = kem.newDecapsulator(kp.getPrivate()); + SecretKey decapsulated = + decapsulator.decapsulate(encapsulated.encapsulation(), 0, 32, "AES"); + assertEquals("AES", decapsulated.getAlgorithm()); + assertArrayEquals(encapsulated.key().getEncoded(), decapsulated.getEncoded()); + } + } + } +} diff --git a/scripts/export_to_ag.py b/scripts/export_to_ag.py index 5328d2c2e..1286a3142 100755 --- a/scripts/export_to_ag.py +++ b/scripts/export_to_ag.py @@ -163,28 +163,42 @@ def setup_kokoro_git_env() -> None: # If git-remote-sso is missing (e.g. in Kokoro GCP Docker container), # set up git-cookie-authdaemon and HTTPS URL rewrites. if not shutil.which("git-remote-sso"): - artifacts_dir = pathlib.Path( - os.environ.get("KOKORO_ARTIFACTS_DIR", "/tmpfs/src") + res_cookie = subprocess.run( + [real_git, "config", "--get", "http.cookiefile"], + capture_output=True, + text=True, + check=False, ) - gcompute_dir = artifacts_dir / "gcompute-tools" - if not gcompute_dir.is_dir(): - subprocess.run( - [ - real_git, - "clone", - "--depth", - "1", - "https://gerrit.googlesource.com/gcompute-tools", - str(gcompute_dir), - ], - capture_output=True, - check=False, - ) - auth_daemon = gcompute_dir / "git-cookie-authdaemon" - if auth_daemon.is_file(): - subprocess.run( - [sys.executable, str(auth_daemon)], capture_output=True, check=False + cookie_file = ( + res_cookie.stdout.strip() if res_cookie.returncode == 0 else "" + ) + if not cookie_file or not pathlib.Path(cookie_file).is_file(): + artifacts_dir = pathlib.Path( + os.environ.get("KOKORO_ARTIFACTS_DIR", "/tmpfs/src") ) + gcompute_dir = artifacts_dir / "gcompute-tools" + if not gcompute_dir.is_dir(): + subprocess.run( + [ + real_git, + "clone", + "--depth", + "1", + "https://gerrit.googlesource.com/gcompute-tools", + str(gcompute_dir), + ], + capture_output=True, + check=False, + ) + auth_daemon = gcompute_dir / "git-cookie-authdaemon" + if auth_daemon.is_file(): + subprocess.run( + [sys.executable, str(auth_daemon)], + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) subprocess.run( [ @@ -433,7 +447,7 @@ def resolve_android_and_build_top( "git", "clone", "--branch", - "master", + "main", url, str(temp_path), ], @@ -441,6 +455,20 @@ def resolve_android_and_build_top( text=True, check=False, ) + if res.returncode != 0: + res = subprocess.run( + [ + "git", + "clone", + "--branch", + "master", + url, + str(temp_path), + ], + capture_output=True, + text=True, + check=False, + ) if res.returncode != 0: res = subprocess.run( [ @@ -587,7 +615,7 @@ def step_copybara_export( "git", "fetch", remote_name, - f"+refs/heads/master:refs/remotes/{remote_name}/master", + f"+refs/heads/main:refs/remotes/{remote_name}/master", ], cwd=android_dir, capture_output=True, @@ -600,7 +628,7 @@ def step_copybara_export( "git", "fetch", remote_name, - f"+refs/heads/main:refs/remotes/{remote_name}/master", + f"+refs/heads/master:refs/remotes/{remote_name}/master", ], cwd=android_dir, capture_output=True, @@ -1111,6 +1139,52 @@ def step_repackage_android( print("[Notice] Could not run currysrc repackaging.") +def _fetch_android_clang_format() -> Tuple[Optional[str], Optional[str]]: + """Downloads Android clang-stable git-clang-format and clang-format from Gitiles if missing locally.""" + cache_dir = pathlib.Path("/tmpfs/bin") + if not cache_dir.is_dir() or not os.access(cache_dir, os.W_OK): + cache_dir = pathlib.Path( + f"/tmp/conscrypt_clang_format_{os.environ.get('USER', 'kokoro')}" + ) + cache_dir.mkdir(parents=True, exist_ok=True) + + gcf_path = cache_dir / "git-clang-format" + cf_path = cache_dir / "clang-format" + if gcf_path.is_file() and cf_path.is_file(): + return str(gcf_path), str(cf_path) + + hosts = [ + "https://android.googlesource.com", + "https://googleplex-android.googlesource.com", + ] + for fname, dest in ( + ("git-clang-format", gcf_path), + ("clang-format", cf_path), + ): + if dest.is_file() and dest.stat().st_size > 0: + continue + fetched = None + for host in hosts: + url = ( + f"{host}/platform/prebuilts/clang/host/linux-x86/+/refs/heads/main/" + f"clang-stable/bin/{fname}?format=TEXT" + ) + try: + b64_data = urllib.request.urlopen(url, timeout=30).read() + fetched = base64.b64decode(b64_data) + if fetched: + break + except Exception: # pylint: disable=broad-except + continue + if not fetched: + print(f"Failed to download {fname} from Android Gitiles.") + return None, None + dest.write_bytes(fetched) + dest.chmod(0o755) + print(f"Downloaded Android clang-format tools to {cache_dir}.") + return str(gcf_path), str(cf_path) + + def step_format_and_commit_android( android_dir: pathlib.Path, build_top: Optional[pathlib.Path], @@ -1162,11 +1236,18 @@ def step_format_and_commit_android( if git_clang_format and clang_format: break + if not git_clang_format or not clang_format: + dl_gcf, dl_cf = _fetch_android_clang_format() + if dl_gcf and not git_clang_format: + git_clang_format = dl_gcf + if dl_cf and not clang_format: + clang_format = dl_cf + if not git_clang_format: git_clang_format = shutil.which("git-clang-format") if git_clang_format: - cmd = [git_clang_format] + cmd = [sys.executable, git_clang_format] if clang_format: cmd.extend(["--binary", clang_format]) cmd.append("HEAD~1") @@ -1219,7 +1300,16 @@ def step_format_and_commit_android( # Ensure Gerrit Change-Id footer is present if "Change-Id:" not in msg: - if cl: + base_res = subprocess.run( + ["git", "rev-parse", "HEAD~1"], + cwd=android_dir, + capture_output=True, + text=True, + check=False, + ) + if base_res.returncode == 0 and base_res.stdout.strip(): + seed = f"conscrypt-export-base-{base_res.stdout.strip()}" + elif cl: seed = f"conscrypt-export-{cl}" else: seed = subprocess.check_output( @@ -1255,30 +1345,52 @@ def step_upload_gerrit(android_dir: pathlib.Path, upload: bool) -> None: uploaded = False last_err = "" + label_opt_sets = [ + ["-o", "l=Presubmit-Ready+1"], + [], + ] for url in push_urls: - print(f"==> Attempting push to {url}...") - res = subprocess.run( - [ - "git", - "push", - "-o", - "nokeycheck", - url, - "HEAD:refs/for/master", - ], - cwd=android_dir, - capture_output=True, - text=True, - check=False, - ) - if res.stdout: - print(res.stdout) - if res.stderr: - print(res.stderr, file=sys.stderr) - if res.returncode == 0: - uploaded = True + for target_branch in ("main", "master"): + for label_opts in label_opt_sets: + label_desc = " with Presubmit-Ready+1" if label_opts else "" + print( + f"==> Attempting push to {url}" + f" (HEAD:refs/for/{target_branch}){label_desc}..." + ) + res = subprocess.run( + [ + "git", + "push", + "-o", + "nokeycheck", + *label_opts, + url, + f"HEAD:refs/for/{target_branch}", + ], + cwd=android_dir, + capture_output=True, + text=True, + check=False, + ) + if res.stdout: + print(res.stdout) + if res.stderr: + print(res.stderr, file=sys.stderr) + if res.returncode == 0: + uploaded = True + break + last_err = res.stderr.strip() + if label_opts and "Presubmit-Ready" in last_err: + print( + "Notice: Presubmit-Ready+1 label not permitted yet for this" + " account; retrying upload without label..." + ) + continue + break + if uploaded: + break + if uploaded: break - last_err = res.stderr.strip() if not uploaded: sys.exit( @@ -1299,7 +1411,7 @@ def step_upload_gerrit(android_dir: pathlib.Path, upload: bool) -> None: print("To upload to Gerrit, run:") print( f" cd {android_dir} && git push -o nokeycheck {GERRIT_SSO_URL}" - " HEAD:refs/for/master" + " HEAD:refs/for/main" ) diff --git a/testing/src/main/java/org/conscrypt/TestUtils.java b/testing/src/main/java/org/conscrypt/TestUtils.java index 8ba7b3b47..c6a7897b9 100644 --- a/testing/src/main/java/org/conscrypt/TestUtils.java +++ b/testing/src/main/java/org/conscrypt/TestUtils.java @@ -150,14 +150,18 @@ private static Provider getNonConscryptTlsProvider() { static Provider getNonConscryptProviderFor(String type, String algorithm) { for (Provider p : Security.getProviders()) { - if (!p.getClass().getPackage().getName().contains("conscrypt") - && (p.getService(type, algorithm) != null)) { + if (!isConscrypt(p) && (p.getService(type, algorithm) != null)) { return p; } } return null; } + // Conscrypt.isConscrypt(Provider) is not part of conscrypt.module.platform.api.stubs. + public static boolean isConscrypt(Provider provider) { + return provider != null && provider.getClass().getPackage().getName().contains("conscrypt"); + } + static Provider getJdkProvider() { return JDK_PROVIDER; }