diff --git a/api/android/revanced-library.api b/api/android/revanced-library.api index 3deedb0..763f582 100644 --- a/api/android/revanced-library.api +++ b/api/android/revanced-library.api @@ -47,6 +47,18 @@ public final class app/revanced/library/ApkUtils$PrivateKeyCertificatePairDetail public final fun getValidUntil ()Ljava/util/Date; } +public final class app/revanced/library/CryptographyKt { + public static final fun getPublicKey (Lorg/bouncycastle/openpgp/PGPPublicKeyRing;)Lorg/bouncycastle/openpgp/PGPPublicKey; + public static final fun getPublicKeyRing (Lorg/bouncycastle/openpgp/PGPPublicKeyRingCollection;J)Lorg/bouncycastle/openpgp/PGPPublicKeyRing; + public static final fun getPublicKeyRingCollection (Ljava/io/InputStream;)Lorg/bouncycastle/openpgp/PGPPublicKeyRingCollection; + public static final fun getSignature (Ljava/io/InputStream;)Lorg/bouncycastle/openpgp/PGPSignature; + public static final fun matchGitHub (Ldev/sigstore/fulcio/client/ImmutableFulcioCertificateMatcher$Builder;Ljava/lang/String;)Ldev/sigstore/fulcio/client/ImmutableFulcioCertificateMatcher$Builder; + public static final fun verificationOptions (Lkotlin/jvm/functions/Function1;)Ldev/sigstore/VerificationOptions; + public static final fun verifySLSA ([BLjava/io/InputStream;Ldev/sigstore/VerificationOptions;)Z + public static final fun verifySLSA ([BLjava/io/InputStream;Lkotlin/jvm/functions/Function1;)Z + public static final fun verifySignature ([BLorg/bouncycastle/openpgp/PGPSignature;Lorg/bouncycastle/openpgp/PGPPublicKey;)Z +} + public final class app/revanced/library/OptionsKt { public static final fun setOptions (Ljava/util/Set;Ljava/util/Map;)V } @@ -97,7 +109,7 @@ public abstract interface class app/revanced/library/installation/command/RunRes public abstract fun getError ()Ljava/lang/String; public abstract fun getExitCode ()I public abstract fun getOutput ()Ljava/lang/String; - public abstract fun waitFor ()V + public fun waitFor ()V } public final class app/revanced/library/installation/command/RunResult$DefaultImpls { diff --git a/api/jvm/revanced-library.api b/api/jvm/revanced-library.api index 81007f9..3a5a797 100644 --- a/api/jvm/revanced-library.api +++ b/api/jvm/revanced-library.api @@ -47,6 +47,18 @@ public final class app/revanced/library/ApkUtils$PrivateKeyCertificatePairDetail public final fun getValidUntil ()Ljava/util/Date; } +public final class app/revanced/library/CryptographyKt { + public static final fun getPublicKey (Lorg/bouncycastle/openpgp/PGPPublicKeyRing;)Lorg/bouncycastle/openpgp/PGPPublicKey; + public static final fun getPublicKeyRing (Lorg/bouncycastle/openpgp/PGPPublicKeyRingCollection;J)Lorg/bouncycastle/openpgp/PGPPublicKeyRing; + public static final fun getPublicKeyRingCollection (Ljava/io/InputStream;)Lorg/bouncycastle/openpgp/PGPPublicKeyRingCollection; + public static final fun getSignature (Ljava/io/InputStream;)Lorg/bouncycastle/openpgp/PGPSignature; + public static final fun matchGitHub (Ldev/sigstore/fulcio/client/ImmutableFulcioCertificateMatcher$Builder;Ljava/lang/String;)Ldev/sigstore/fulcio/client/ImmutableFulcioCertificateMatcher$Builder; + public static final fun verificationOptions (Lkotlin/jvm/functions/Function1;)Ldev/sigstore/VerificationOptions; + public static final fun verifySLSA ([BLjava/io/InputStream;Ldev/sigstore/VerificationOptions;)Z + public static final fun verifySLSA ([BLjava/io/InputStream;Lkotlin/jvm/functions/Function1;)Z + public static final fun verifySignature ([BLorg/bouncycastle/openpgp/PGPSignature;Lorg/bouncycastle/openpgp/PGPPublicKey;)Z +} + public final class app/revanced/library/OptionsKt { public static final fun setOptions (Ljava/util/Set;Ljava/util/Map;)V } @@ -73,7 +85,7 @@ public abstract interface class app/revanced/library/installation/command/RunRes public abstract fun getError ()Ljava/lang/String; public abstract fun getExitCode ()I public abstract fun getOutput ()Ljava/lang/String; - public abstract fun waitFor ()V + public fun waitFor ()V } public final class app/revanced/library/installation/command/RunResult$DefaultImpls { diff --git a/build.gradle.kts b/build.gradle.kts index 5ccaf50..a5798b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -54,7 +54,9 @@ kotlin { commonMain.dependencies { implementation(libs.apksig) implementation(libs.apkzlib) - implementation(libs.bcpkix.jdk18on) + implementation(libs.bouncycastle.bcpkix) + implementation(libs.bouncycastle.pgp) + implementation(libs.sigstore.java) implementation(libs.guava) implementation(libs.jadb) implementation(libs.kotlin.reflect) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d1cde0c..3391d38 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,20 +1,20 @@ [versions] -android = "8.5.2" -bcpkix-jdk18on = "1.77" -binary-compatibility-validator = "0.15.1" -core-ktx = "1.15.0" -guava = "33.2.1-jre" +android = "8.12.3" +binary-compatibility-validator = "0.18.1" +core-ktx = "1.17.0" +guava = "33.5.0-jre" jadb = "1.2.1.1" -kotlin = "2.0.20" -kotlinx-coroutines = "1.8.1" -kotlinx-serialization = "1.7.1" +kotlin = "2.2.21" +kotlinx-coroutines = "1.10.2" +kotlinx-serialization = "1.9.0" libsu = "5.2.2" revanced-patcher = "21.0.0" +bouncy-castle = "1.82" +sigstore = "2.0.0-rc2" [libraries] apkzlib = { module = "com.android.tools.build:apkzlib", version.ref = "android" } apksig = { module = "com.android.tools.build:apksig", version.ref = "android" } -bcpkix-jdk18on = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bcpkix-jdk18on" } core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } guava = { module = "com.google.guava:guava", version.ref = "guava" } jadb = { module = "app.revanced:jadb", version.ref = "jadb" } # Fork with Shell v2 support. @@ -26,6 +26,9 @@ libsu-core = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" libsu-nio = { module = "com.github.topjohnwu.libsu:nio", version.ref = "libsu" } libsu-service = { module = "com.github.topjohnwu.libsu:service", version.ref = "libsu" } revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" } +bouncycastle-bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bouncy-castle" } +bouncycastle-pgp = { module = "org.bouncycastle:bcpg-jdk18on", version.ref = "bouncy-castle" } +sigstore-java = { module = "dev.sigstore:sigstore-java", version.ref = "sigstore" } [plugins] android-library = { id = "com.android.library", version.ref = "android" } diff --git a/src/commonMain/kotlin/app/revanced/library/ApkSigner.kt b/src/commonMain/kotlin/app/revanced/library/ApkSigner.kt index e020576..a9f51a6 100644 --- a/src/commonMain/kotlin/app/revanced/library/ApkSigner.kt +++ b/src/commonMain/kotlin/app/revanced/library/ApkSigner.kt @@ -1,6 +1,7 @@ package app.revanced.library import com.android.apksig.ApkSigner.SignerConfig +import com.android.apksig.KeyConfig import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.cert.X509v3CertificateBuilder @@ -155,12 +156,11 @@ object ApkSigner { // Read the private key and certificate from the keystore. - val privateKey = - try { - keyStore.getKey(keyStoreEntryAlias, keyStoreEntryPassword.toCharArray()) as PrivateKey - } catch (exception: UnrecoverableKeyException) { - throw IllegalArgumentException("Invalid password for keystore entry $keyStoreEntryAlias") - } + val privateKey = try { + keyStore.getKey(keyStoreEntryAlias, keyStoreEntryPassword.toCharArray()) as PrivateKey + } catch (exception: UnrecoverableKeyException) { + throw IllegalArgumentException("Invalid password for keystore entry $keyStoreEntryAlias") + } val certificate = keyStore.getCertificate(keyStoreEntryAlias) as X509Certificate @@ -186,7 +186,7 @@ object ApkSigner { listOf( SignerConfig.Builder( signer, - privateKeyCertificatePair.privateKey, + KeyConfig.Jca(privateKeyCertificatePair.privateKey), listOf(privateKeyCertificatePair.certificate), ).build(), ), diff --git a/src/commonMain/kotlin/app/revanced/library/Cryptography.kt b/src/commonMain/kotlin/app/revanced/library/Cryptography.kt new file mode 100644 index 0000000..2b2ef60 --- /dev/null +++ b/src/commonMain/kotlin/app/revanced/library/Cryptography.kt @@ -0,0 +1,163 @@ +@file:Suppress("unused") + +package app.revanced.library + +import dev.sigstore.KeylessVerifier +import dev.sigstore.VerificationOptions +import dev.sigstore.VerificationOptions.CertificateMatcher +import dev.sigstore.bundle.Bundle +import dev.sigstore.dsse.InTotoPayload +import dev.sigstore.fulcio.client.ImmutableFulcioCertificateMatcher.Builder +import dev.sigstore.strings.StringMatcher +import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator +import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider +import java.io.InputStream + +// region PGP signature verification. + +private val verifierBuilderProvider = BcPGPContentVerifierBuilderProvider() + +private val fingerprintCalculator = BcKeyFingerprintCalculator() + +/** + * Verifies the PGP signature of the provided bytes using the provided signature and public key. + * + * @param bytes The bytes to verify. + * @param signature The PGP signature. + * @param publicKey The PGP public key. + * @return True if the signature is valid, false otherwise. + */ +fun verifySignature( + bytes: ByteArray, signature: PGPSignature, publicKey: PGPPublicKey +) = signature.apply { + init(verifierBuilderProvider, publicKey) + update(bytes) +}.verify() + +/** + * Gets the PGP signature from the provided signature input stream. + * + * @param signatureInputStream The input stream of the PGP signature. + * @return The PGP signature. + * @throws IllegalArgumentException if the signature format is invalid. + */ +fun getSignature( + signatureInputStream: InputStream +) = when (val pgpObject = PGPObjectFactory( + PGPUtil.getDecoderStream(signatureInputStream), fingerprintCalculator +).nextObject()) { + is PGPSignatureList -> pgpObject + is PGPCompressedData -> { + val compressedDataFactory = PGPObjectFactory( + pgpObject.dataStream, fingerprintCalculator + ) + compressedDataFactory.nextObject() as PGPSignatureList + } + + else -> throw IllegalArgumentException("Invalid PGP signature format.") +}.first() + + +/** + * Gets the PGP public key ring collection from the provided public key ring input stream. + * + * @param publicKeyRingInputStream The input stream of the public key ring. + * @return The PGP public key ring collection. + */ +fun getPublicKeyRingCollection( + publicKeyRingInputStream: InputStream +) = PGPPublicKeyRingCollection(PGPUtil.getDecoderStream(publicKeyRingInputStream), fingerprintCalculator) + +/** + * Gets the PGP public key ring with the specified key ID from the provided public key ring collection. + * + * @param publicKeyRingCollection The PGP public key ring collection. + * @param keyId The key ID of the public key ring to retrieve. + * @return The PGP public key ring with the specified key ID. + * @throws IllegalArgumentException if the public key ring with the specified key ID is not found. + */ +fun getPublicKeyRing( + publicKeyRingCollection: PGPPublicKeyRingCollection, keyId: Long +) = publicKeyRingCollection.getPublicKeyRing(keyId) + ?: throw IllegalArgumentException("Can't find public key ring with ID $keyId.") + +/** + * Gets the PGP public key from the provided public key ring. + */ +fun getPublicKey(publicKeyRing: PGPPublicKeyRing): PGPPublicKey = publicKeyRing.publicKey + +// endregion + +// region SLSA attestation verification. + +private val keylessVerifier: KeylessVerifier = KeylessVerifier.builder().sigstorePublicDefaults().build() + +private const val RUNNER_ENVIRONMENT_OID = "1.3.6.1.4.1.57264.1.11" + +private const val PROVENANCE_PREDICATE_TYPE = "https://slsa.dev/provenance/v1" + +/** + * Verifies the SLSA attestation of the provided digest using the provided attestation input stream and matcher. + * + * @param digest The digest to verify. + * @param attestationInputStream The input stream of the attestation. + * @param matcher The matcher to add to the verification options. + * @return True if the verification is successful, false otherwise. + */ +fun verifySLSA( + digest: ByteArray, + attestationInputStream: InputStream, + matcher: Builder.() -> Builder, +) = verifySLSA(digest, attestationInputStream, verificationOptions(matcher)) + +/** + * Verifies the SLSA attestation of the provided digest using the provided attestation input stream + * and verification options. + * + * @param digest The digest to verify. + * @param attestationInputStream The input stream of the attestation. + * @param verificationOptions The verification options to use. + * @return True if the verification is successful, false otherwise. + */ +fun verifySLSA( + digest: ByteArray, + attestationInputStream: InputStream, + verificationOptions: VerificationOptions, +) = runCatching { + val bundle = Bundle.from(attestationInputStream.reader()) + + val predicateType = InTotoPayload.from(bundle.dsseEnvelope.get()).predicateType + require(predicateType == PROVENANCE_PREDICATE_TYPE) + + keylessVerifier.verify(digest, bundle, verificationOptions) +}.isSuccess + +/** + * Creates verification options with the provided matcher. + * + * @param matcher The matcher to add to the verification options. + * @return The created verification options. + */ +fun verificationOptions( + matcher: Builder.() -> Builder +): VerificationOptions = VerificationOptions.builder().addCertificateMatchers( + CertificateMatcher.fulcio().matcher().build() +).build() + +/** + * Adds GitHub-specific matching to the builder for the specified repository. + * + * @param repository The GitHub repository in the format "owner/repo". + * @return The updated builder with GitHub-specific matching. + */ +fun Builder.matchGitHub( + repository: String +): Builder = issuer( + StringMatcher.string("https://token.actions.githubusercontent.com") +).subjectAlternativeName( + StringMatcher.regex("(?i)^https://github.com/$repository") +).putOidDerAsn1Strings(RUNNER_ENVIRONMENT_OID, StringMatcher.string("github-hosted")) + +// endregion +