diff --git a/apilevel/levels.go b/apilevel/levels.go index be3c46f..c26e408 100644 --- a/apilevel/levels.go +++ b/apilevel/levels.go @@ -55,6 +55,10 @@ func SupportsSigV3(level int32) bool { return level >= V9_0_Pie } +func SupportsSigV31(level int32) bool { + return level >= V13_0_TIRAMISU +} + func RequiresSandboxV2(level int32) bool { return level >= V8_0_Oreo } diff --git a/apkverifier.go b/apkverifier.go index aca3900..556e7f9 100644 --- a/apkverifier.go +++ b/apkverifier.go @@ -17,17 +17,17 @@ import ( "github.com/avast/apkverifier/signingblock" ) -// Contains result of Apk verification +// Result Contains result of Apk verification type Result struct { SigningSchemeId int SignerCerts [][]*x509.Certificate SigningBlockResult *signingblock.VerificationResult } -// Returned from the Verify method if the file starts with the DEX magic value, +// ErrMixedDexApkFile Returned from the Verify method if the file starts with the DEX magic value, // but otherwise looks like a properly signed APK. // -// This detect 'Janus' Android vulnerability where a DEX is prepended to a valid, +// This detects 'Janus' Android vulnerability where a DEX is prepended to a valid, // signed APK file. The signature verification passes because with v1 scheme, // only the APK portion of the file is checked, but Android then loads the prepended, // unsigned DEX file instead of the one from APK. @@ -39,19 +39,21 @@ var ErrMixedDexApkFile = errors.New("This file is both DEX and ZIP archive! Expl const ( dexHeaderMagic uint32 = 0xa786564 // "dex\n", littleendinan + + maxApkSigners = 10 ) -// Calls VerifyWithSdkVersion with sdk versions +// Verify Calls VerifyWithSdkVersion with sdk versions func Verify(path string, optionalZip *apkparser.ZipReader) (res Result, err error) { return VerifyWithSdkVersion(path, optionalZip, apilevel.V_AnyMin, apilevel.V_AnyMax) } -// Calls VerifyWithSdkVersionReader with sdk versions +// VerifyReader Calls VerifyWithSdkVersionReader with sdk versions func VerifyReader(r io.ReadSeeker, optionalZip *apkparser.ZipReader) (res Result, err error) { return VerifyWithSdkVersionReader(r, optionalZip, apilevel.V_AnyMin, apilevel.V_AnyMax) } -// see VerifyWithSdkVersionReader +// VerifyWithSdkVersion see VerifyWithSdkVersionReader func VerifyWithSdkVersion(path string, optionalZip *apkparser.ZipReader, minSdkVersion, maxSdkVersion int32) (res Result, err error) { f, err := os.Open(path) if err != nil { @@ -61,7 +63,7 @@ func VerifyWithSdkVersion(path string, optionalZip *apkparser.ZipReader, minSdkV return VerifyWithSdkVersionReader(f, optionalZip, minSdkVersion, maxSdkVersion) } -// Verify the application signature. If err is nil, the signature is correct, +// VerifyWithSdkVersionReader Verify the application signature. If err is nil, the signature is correct, // otherwise it is not and res may or may not contain extracted certificates, // depending on how the signature verification failed. // Path is required, pass optionalZip if you have the ZipReader already opened and want to reuse it. @@ -78,11 +80,11 @@ func VerifyWithSdkVersionReader(r io.ReadSeeker, optionalZip *apkparser.ZipReade defer optionalZip.Close() } - var sandboxVersion int32 + var sandboxVersion, targetSdkVersion int32 var manifestError error if minSdkVersion == apilevel.V_AnyMin || apilevel.RequiresSandboxV2(maxSdkVersion) { var manifestMinSdkVersion int32 - manifestMinSdkVersion, sandboxVersion, err = getManifestInfo(optionalZip) + manifestMinSdkVersion, targetSdkVersion, sandboxVersion, err = getManifestInfo(optionalZip) if err != nil { manifestError = err } else { @@ -119,6 +121,8 @@ func VerifyWithSdkVersionReader(r io.ReadSeeker, optionalZip *apkparser.ZipReade var sandboxError error if apilevel.RequiresSandboxV2(maxSdkVersion) && sandboxVersion > 1 && (signingBlockError != nil || res.SigningSchemeId < 2) { sandboxError = fmt.Errorf("no valid signature for sandbox version %d", sandboxVersion) + } else if targetSdkVersion >= apilevel.V11_0_Eleven && maxSdkVersion >= targetSdkVersion && res.SigningSchemeId < 2 { + sandboxError = fmt.Errorf("target SDK version %d requires a minimum of signature scheme v2; the APK is not signed with this or a later signature scheme", targetSdkVersion) } var certsv1 [][]*x509.Certificate @@ -140,7 +144,7 @@ func VerifyWithSdkVersionReader(r io.ReadSeeker, optionalZip *apkparser.ZipReade return } -// Extract certs without verifying the signature. +// ExtractCerts Extract certs without verifying the signature. func ExtractCerts(path string, optionalZip *apkparser.ZipReader) ([][]*x509.Certificate, error) { f, err := os.Open(path) if err != nil { @@ -172,8 +176,9 @@ func ExtractCertsReader(r io.ReadSeeker, optionalZip *apkparser.ZipReader) ([][] } type sandboxVersionEncoder struct { - minSdkVersion int32 - sandboxVersion int32 + minSdkVersion int32 + targetSdkVersion int32 + sandboxVersion int32 } func (e *sandboxVersionEncoder) EncodeToken(t xml.Token) error { @@ -197,6 +202,14 @@ func (e *sandboxVersionEncoder) EncodeToken(t xml.Token) error { } else if err != io.EOF { return err } + + val, err = e.getAttrIntValue(&st, "targetSdkVersion") + if err == nil { + e.targetSdkVersion = val + } else if err != io.EOF { + return err + } + return apkparser.ErrEndParsing } return nil @@ -219,10 +232,10 @@ func (e *sandboxVersionEncoder) getAttrIntValue(st *xml.StartElement, name strin return 0, io.EOF } -func getManifestInfo(zip *apkparser.ZipReader) (minSdkVersion, sandboxVersion int32, err error) { +func getManifestInfo(zip *apkparser.ZipReader) (minSdkVersion, targetSdkVersion, sandboxVersion int32, err error) { manifest := zip.File["AndroidManifest.xml"] if manifest == nil { - return 1, 1, nil + return 1, 0, 1, nil } if err = manifest.Open(); err != nil { @@ -232,12 +245,12 @@ func getManifestInfo(zip *apkparser.ZipReader) (minSdkVersion, sandboxVersion in defer manifest.Close() for manifest.Next() { - enc := sandboxVersionEncoder{1, 1} + enc := sandboxVersionEncoder{1, 0, 1} if err = apkparser.ParseXml(manifest, &enc, nil); err != nil { err = fmt.Errorf("failed to parse AndroidManifest.xml: %s", err.Error()) return } - return enc.minSdkVersion, enc.sandboxVersion, nil + return enc.minSdkVersion, enc.targetSdkVersion, enc.sandboxVersion, nil } return } diff --git a/apkverifier_test.go b/apkverifier_test.go index 114b3dc..cb613e6 100644 --- a/apkverifier_test.go +++ b/apkverifier_test.go @@ -35,7 +35,7 @@ func Example() { } } -// From https://android.googlesource.com/platform/tools/apksig 5941d7112b28d5dada825aad757781f0b3ecf23b +// From https://android.googlesource.com/platform/tools/apksig c5b2567d87833f347cd578acc0262501c7eae423 var ( DSA_KEY_NAMES = []string{"1024", "2048", "3072"} DSA_KEY_NAMES_1024_AND_SMALLER = []string{"1024"} @@ -45,6 +45,17 @@ var ( RSA_KEY_NAMES_2048_AND_LARGER = []string{"2048", "3072", "4096", "8192", "16384"} ) +const ( + RSA_2048_CHUNKED_SHA256_DIGEST = "0a457e6dd7cc8d4dde28a4dae843032de5fbe58123eedd0a31e7f958f23e1626" + RSA_2048_CHUNKED_SHA256_DIGEST_FROM_INCORRECTLY_SIGNED_APK = "0a457e6dd7cc8d4dde28a4dae843032de5fbe58101eedd0a31e7f958f23e1626" + + FIRST_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048" + SECOND_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_2" + THIRD_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_3" + EC_P256_SIGNER_RESOURCE_NAME = "ec-p256" + EC_P256_2_SIGNER_RESOURCE_NAME = "ec-p256_2" +) + const anyErrorString = "LiterallyAnything" func TestOriginalAccepted(t *testing.T) { @@ -152,6 +163,14 @@ func TestV1OneSignerSHA256withDSAAccepted(t *testing.T) { assertVerifiedForEach(t, "v1-only-with-dsa-sha256-2.16.840.1.101.3.4.3.2-%s.apk", DSA_KEY_NAMES) } +func TestV1MaxSupportedSignersAccepted(t *testing.T) { + assertVerified(t, "v1-only-10-signers.apk") +} + +func TestV1MoreThanMaxSupportedSignersRejected(t *testing.T) { + assertVerificationFailure(t, "v1-only-11-signers.apk", "APK Signature Scheme v1 only supports a maximum") +} + func TestV2StrippedRejected(t *testing.T) { // APK signed with v1 and v2 schemes, but v2 signature was stripped from the file (by using // zipalign). @@ -423,6 +442,14 @@ func TestV2TwoSignersRejectedWhenOneWithoutSupportedSignatures(t *testing.T) { assertVerificationFailure(t, "v2-only-two-signers-second-signer-no-supported-sig.apk", "no supported signatures found") } +func TestV2MaxSupportedSignersAccepted(t *testing.T) { + assertVerifiedSdk(t, "v2-only-10-signers.apk", apilevel.V7_1_Nougat, apilevel.V_AnyMax) +} + +func TestV2MoreThanMaxSupportedSignersRejected(t *testing.T) { + assertVerificationFailureSdk(t, "v2-only-11-signers.apk", apilevel.V7_1_Nougat, apilevel.V_AnyMax, "APK Signature Scheme V2 only supports a maximum") +} + func TestCorrectCertUsedFromPkcs7SignedDataCertsSet(t *testing.T) { // Obtained by prepending the rsa-1024 certificate to the PKCS#7 SignedData certificates set // of v1-only-with-rsa-pkcs1-sha1-1.2.840.113549.1.1.1-2048.apk META-INF/CERT.RSA. The certs @@ -533,6 +560,23 @@ func TestTargetSandboxVersion2AndHigher(t *testing.T) { assertVerified(t, "v2-only-targetSandboxVersion-3.apk") } +func TestTargetSdkMinSchemeVersionNotMet(t *testing.T) { + assertVerificationFailure(t, "v1-ec-p256-targetSdk-30.apk", "target SDK version 30 requires a minimum of signature scheme v2") +} + +func TestTargetSdkMinSchemeVersionMet(t *testing.T) { + assertVerified(t, "v2-ec-p256-targetSdk-30.apk") + assertVerified(t, "v3-ec-p256-targetSdk-30.apk") +} + +func TestTargetSdkMinSchemeVersionNotMetMaxLessThanTarget(t *testing.T) { + assertVerifiedSdk(t, "v1-ec-p256-targetSdk-30.apk", apilevel.V_AnyMin, apilevel.V10_0_Ten) +} + +func TestTargetSdkNoUsesSdkElement(t *testing.T) { + assertVerified(t, "v1-only-no-uses-sdk.apk") +} + func TestV1MultipleDigestAlgsInManifestAndSignatureFile(t *testing.T) { // MANIFEST.MF contains SHA-1 and SHA-256 digests for each entry, .SF contains only SHA-1 // digests. This file was obtained by: @@ -699,46 +743,71 @@ func TestV1SignedAttrsWrongSignature(t *testing.T) { // Lineage tests func TestLineageFromAPKContainsExpectedSigners(t *testing.T) { - res := assertVerifiedSdk(t, "v1v2v3-with-rsa-2048-lineage-3-signers.apk", apilevel.V7_0_Nougat, apilevel.V_AnyMax) + assertLineageHasCerts(t, "v1v2v3-with-rsa-2048-lineage-3-signers.apk", apilevel.V7_0_Nougat, []string{ + FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, THIRD_RSA_2048_SIGNER_RESOURCE_NAME, + }) +} + +func TestGetResultLineage(t *testing.T) { + assertLineageHasCerts(t, "v31-tgt-33-no-v3-attr.apk", apilevel.V8_0_Oreo, []string{ + FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, + }) +} + +func TestGetResultV3Lineage(t *testing.T) { + assertLineageHasCerts(t, "v3-rsa-2048_2-tgt-dev-release.apk", apilevel.V7_0_Nougat, []string{ + FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, + }) +} + +func TestGetResultNoLineage(t *testing.T) { + res := assertVerificationFailureSdk(t, "v31-empty-lineage-no-v3.apk", apilevel.V7_0_Nougat, apilevel.V_AnyMax, + "The APK contains a v3.1 signing block without a v3.0 base block") if res.SigningBlockResult == nil { t.Fatalf("no signing block found") - } else if res.SigningBlockResult.SigningLineage == nil { - t.Fatalf("no signing lineage found") + } else if res.SigningBlockResult.SigningLineage != nil { + t.Fatalf("signing lineage found") } +} - lin := res.SigningBlockResult.SigningLineage +func TestGetResultNoV31Apk(t *testing.T) { + res := assertVerifiedSdk(t, "v3-rsa-2048_2-tgt-dev-release.apk", apilevel.V7_0_Nougat, apilevel.V_AnyMax) + if res.SigningBlockResult == nil { + t.Fatalf("no signing block found") + } - certNames := []string{"rsa-2048.x509.pem", "rsa-2048_2.x509.pem", "rsa-2048_3.x509.pem"} - if len(certNames) != len(lin.Nodes) { - t.Fatalf("invalid number of certs in lineage, expected %d got %d", len(certNames), len(lin.Nodes)) + if res.SigningSchemeId != 3 { + t.Fatalf("Invalid scheme id, got %d and expected 3", res.SigningSchemeId) } +} - rsc := filepath.Join(os.Getenv("APKSIG_PATH"), "src/test/resources/com/android/apksig") - for _, cn := range certNames { - data, err := ioutil.ReadFile(filepath.Join(rsc, cn)) - if err != nil { - t.Fatalf("failed to read cert file %s: %s", cn, err.Error()) - } +func TestGetResultFromV3BlockFromV31SignedApk(t *testing.T) { + res := assertVerifiedSdk(t, "v31-rsa-2048_2-tgt-33-1-tgt-28.apk", apilevel.V7_0_Nougat, apilevel.V_AnyMax) + if res.SigningBlockResult == nil { + t.Fatalf("no signing block found") + } - block, _ := pem.Decode(data) + if res.SigningSchemeId != 31 { + t.Fatalf("Invalid scheme id, got %d and expected 31", res.SigningSchemeId) + } - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - t.Fatalf("failed to parse cert %s: %s", cn, err.Error()) - } + v3result := res.SigningBlockResult.ExtraResults[3] + if v3result == nil { + t.Fatalf("Extra V3 result not found") + } - found := false - for _, n := range lin.Nodes { - if n.SigningCert.Equal(cert) { - found = true - break - } - } + if v3result.SigningLineage != nil { + t.Fatalf("signing lineage found") + } +} - if !found { - ci := apkverifier.NewCertInfo(cert) - t.Fatalf("certificate %s was not found in lineage", ci.String()) - } +func TestGetResultContainsLineageErrors(t *testing.T) { + res := assertVerificationFailureSdk(t, "v31-2elem-incorrect-lineage.apk", apilevel.V9_0_Pie, apilevel.V_AnyMax, + "failed to read signing certificate lineage attribute: failed to parse cert in node") + if res.SigningBlockResult == nil { + t.Fatalf("no signing block found") + } else if res.SigningBlockResult.SigningLineage != nil { + t.Fatalf("signing lineage found") } } @@ -780,6 +849,61 @@ func TestAsn1SuperfluousLeadingZeros(t *testing.T) { } } +func TestVerifySignatureNegativeModulusConscryptProvider(t *testing.T) { + assertVerified(t, "v1v2v3-rsa-2048-negmod-in-cert.apk") + assertVerifiedSdk(t, "v1v2v3-rsa-2048-negmod-in-cert.apk", apilevel.V_AnyMin, apilevel.V6_0_Marshmallow) +} + +func TestVerifyV31RotationTarget34(t *testing.T) { + assertVerifiedWithSigners(t, "v31-rsa-2048_2-tgt-10000-1-tgt-28.apk", true, + []string{FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME}) + // assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, 10000); +} + +func TestVerifyV31MissingStrippingAttr(t *testing.T) { + res := assertVerified(t, "v31-tgt-33-no-v3-attr.apk") + assertWarning(t, res, "does not contain the attribute to detect if this signature is stripped") +} + +func TestVerifyV31MissingV31Block(t *testing.T) { + assertVerificationFailureSdk(t, "v31-block-stripped-v3-attr-value-33.apk", apilevel.V10_0_Ten, apilevel.V_AnyMax, + "The v3 signer indicates key rotation should be supported starting from SDK version 33, but a v3.1 block") + assertVerifiedSdk(t, "v31-block-stripped-v3-attr-value-33.apk", apilevel.V_AnyMin, apilevel.V13_0_TIRAMISU-1) +} + +func TestVerifyV31BlockWithoutV3Block(t *testing.T) { + assertVerificationFailure(t, "v31-tgt-33-no-v3-block.apk", "The APK contains a v3.1 signing block without a v3.0 base block") +} + +func TestVerifyV31RotationTargetsDevRelease(t *testing.T) { + assertVerifiedWithSigners(t, "v31-rsa-2048_2-tgt-10000-dev-release.apk", true, + []string{FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME}) +} + +func TestVerifyV3RotatedSignedTargetsDevRelease(t *testing.T) { + res := assertVerified(t, "v3-rsa-2048_2-tgt-dev-release.apk") + assertWarning(t, res, "The rotation-targets-dev-release attribute is only supported on v3.1 signers") +} + +func TestVerifyV31RotationTargets34(t *testing.T) { + assertLineageHasCerts(t, "v31-rsa-2048_2-tgt-34-1-tgt-28.apk", apilevel.V_AnyMin, + []string{FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME}) +} + +func TestVerifyV31MinSdkVersionT(t *testing.T) { + res := assertVerifiedSdk(t, "v31-rsa-2048_2-tgt-33-1-tgt-28.apk", apilevel.V13_0_TIRAMISU, apilevel.V_AnyMax) + if res.SigningSchemeId != 31 { + t.Fatalf("Not using scheme 31, %d", res.SigningSchemeId) + } +} + +func TestVerifyV31MinSdkVersionTTargetSdk30(t *testing.T) { + res := assertVerifiedSdk(t, "v31-ec-p256-2-tgt-33-1-tgt-28-targetSdk-30.apk", apilevel.V13_0_TIRAMISU, apilevel.V_AnyMax) + if res.SigningSchemeId != 31 { + t.Fatalf("Not using scheme 31, %d", res.SigningSchemeId) + } +} + func assertVerifiedForEach(t *testing.T, format string, names []string) { assertVerifiedForEachSdk(t, format, names, apilevel.V_AnyMin, apilevel.V_AnyMax) } @@ -847,6 +971,127 @@ func assertNoLineage(t *testing.T, name string, mustVerify bool, minlevel int32) } } +func assertLineageHasCerts(t *testing.T, name string, minlevel int32, certNames []string) { + res := assertVerifiedSdk(t, name, minlevel, apilevel.V_AnyMax) + if res.SigningBlockResult == nil { + t.Fatalf("no signing block found") + } else if res.SigningBlockResult.SigningLineage == nil { + t.Fatalf("no signing lineage found") + } + + lin := res.SigningBlockResult.SigningLineage + + if len(certNames) != len(lin.Nodes) { + t.Fatalf("invalid number of certs in lineage, expected %d got %d", len(certNames), len(lin.Nodes)) + } + + rsc := filepath.Join(os.Getenv("APKSIG_PATH"), "src/test/resources/com/android/apksig") + for _, cn := range certNames { + data, err := ioutil.ReadFile(filepath.Join(rsc, cn+".x509.pem")) + if err != nil { + t.Fatalf("failed to read cert file %s: %s", cn, err.Error()) + } + + block, _ := pem.Decode(data) + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatalf("failed to parse cert %s: %s", cn, err.Error()) + } + + found := false + for _, n := range lin.Nodes { + if n.SigningCert.Equal(cert) { + found = true + break + } + } + + if !found { + ci := apkverifier.NewCertInfo(cert) + t.Fatalf("certificate %s was not found in lineage", ci.String()) + } + } +} + +func assertVerifiedWithSigners(t *testing.T, name string, rotationExpected bool, certNames []string) apkverifier.Result { + res := assertVerified(t, name) + + var v3CertNames []string + if res.SigningSchemeId >= 3 { + if !rotationExpected || res.SigningSchemeId == 31 { + v3CertNames = certNames[0:1] + } else { + v3CertNames = certNames[len(certNames)-1:] + } + } + + if res.SigningSchemeId == 31 { + assertSigners(t, res.SigningBlockResult.ExtraResults[3].Certs, v3CertNames) + assertSigners(t, res.SignerCerts, certNames[len(certNames)-1:]) + } else if res.SigningSchemeId == 3 { + assertSigners(t, res.SignerCerts, v3CertNames) + } else { + if rotationExpected { + certNames = certNames[0:1] + } + assertSigners(t, res.SignerCerts, certNames) + } + return res +} + +func assertSigners(t *testing.T, signerCerts [][]*x509.Certificate, certNames []string) { + expectedSigners := make(map[[32]byte]*x509.Certificate) + signersNotFound := make(map[[32]byte]string) + rsc := filepath.Join(os.Getenv("APKSIG_PATH"), "src/test/resources/com/android/apksig") + for _, cn := range certNames { + data, err := ioutil.ReadFile(filepath.Join(rsc, cn+".x509.pem")) + if err != nil { + t.Fatalf("failed to read cert file %s: %s", cn, err.Error()) + } + + block, _ := pem.Decode(data) + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatalf("failed to parse cert %s: %s", cn, err.Error()) + } + certHash := sha256.Sum256(cert.Raw) + expectedSigners[certHash] = cert + signersNotFound[certHash] = cn + } + + for _, certs := range signerCerts { + for certHash, expectedCert := range expectedSigners { + if certs[0].Equal(expectedCert) { + delete(signersNotFound, certHash) + } + } + } + + if len(signersNotFound) != 0 { + msg := "Failed to find signers: " + for _, name := range signersNotFound { + msg += name + " " + } + t.Fatal(msg) + } +} + +func assertWarning(t *testing.T, res apkverifier.Result, text string) { + if res.SigningBlockResult == nil { + t.Fatalf("no signing block found") + } + + for _, w := range res.SigningBlockResult.Warnings { + if strings.Contains(w, text) { + return + } + } + + t.Fatalf("was supposed have warning '%s' but not found\n%s", text, formatResult(t, res)) +} + func verify(t *testing.T, name string, minSdkVersion, maxSdkVersion int32) (apkverifier.Result, error) { apksigPath, prs := os.LookupEnv("APKSIG_PATH") if !prs || apksigPath == "" { diff --git a/certinfo.go b/certinfo.go index 1478783..f21b560 100644 --- a/certinfo.go +++ b/certinfo.go @@ -14,7 +14,7 @@ import ( "time" ) -// Nicer looking certificate info +// CertInfo Nicer looking certificate info type CertInfo struct { Md5 string Sha1 string @@ -54,7 +54,7 @@ func (c byPreference) Less(i, j int) bool { return bytes.Compare(ci.Raw, cj.Raw) > 0 } -// Picks the "best-looking" (most likely the correct one) certificate from the chain +// PickBestApkCert Picks the "best-looking" (most likely the correct one) certificate from the chain // extracted from APK. Is noop for most APKs, as they usually contain only one certificate. func PickBestApkCert(chains [][]*x509.Certificate) (*CertInfo, *x509.Certificate) { if len(chains) == 0 { @@ -66,14 +66,14 @@ func PickBestApkCert(chains [][]*x509.Certificate) (*CertInfo, *x509.Certificate return NewCertInfo(chains[0][0]), chains[0][0] } -// Returns new CertInfo with information from the x509.Certificate. +// NewCertInfo Returns new CertInfo with information from the x509.Certificate. func NewCertInfo(cert *x509.Certificate) *CertInfo { var res CertInfo res.Fill(cert) return &res } -// Replaces CertInfo's data with information from the x509.Certificate. +// Fill Replaces CertInfo's data with information from the x509.Certificate. func (ci *CertInfo) Fill(cert *x509.Certificate) { md5sum := md5.Sum(cert.Raw) sha1sum := sha1.Sum(cert.Raw) diff --git a/internal/x509andr/x509.go b/internal/x509andr/x509.go index 2afc752..548d126 100644 --- a/internal/x509andr/x509.go +++ b/internal/x509andr/x509.go @@ -70,7 +70,7 @@ func ParsePKIXPublicKey(derBytes []byte) (pub interface{}, err error) { if algo == UnknownPublicKeyAlgorithm { return nil, errors.New("x509: unknown public key algorithm") } - return parsePublicKey(algo, &pki) + return parsePublicKey(algo, &pki, nil) } func marshalPublicKey(pub interface{}) (publicKeyBytes []byte, publicKeyAlgorithm pkix.AlgorithmIdentifier, err error) { @@ -452,17 +452,19 @@ func getSignatureAlgorithmFromAI(ai pkix.AlgorithmIdentifier) SignatureAlgorithm // RFC 3279, 2.3 Public Key Algorithms // // pkcs-1 OBJECT IDENTIFIER ::== { iso(1) member-body(2) us(840) -// rsadsi(113549) pkcs(1) 1 } +// +// rsadsi(113549) pkcs(1) 1 } // // rsaEncryption OBJECT IDENTIFIER ::== { pkcs1-1 1 } // // id-dsa OBJECT IDENTIFIER ::== { iso(1) member-body(2) us(840) -// x9-57(10040) x9cm(4) 1 } // -// RFC 5480, 2.1.1 Unrestricted Algorithm Identifier and Parameters +// x9-57(10040) x9cm(4) 1 } +// +// # RFC 5480, 2.1.1 Unrestricted Algorithm Identifier and Parameters // -// id-ecPublicKey OBJECT IDENTIFIER ::= { -// iso(1) member-body(2) us(840) ansi-X9-62(10045) keyType(2) 1 } +// id-ecPublicKey OBJECT IDENTIFIER ::= { +// iso(1) member-body(2) us(840) ansi-X9-62(10045) keyType(2) 1 } var ( oidPublicKeyRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} oidPublicKeyDSA = asn1.ObjectIdentifier{1, 2, 840, 10040, 4, 1} @@ -483,18 +485,18 @@ func getPublicKeyAlgorithmFromOID(oid asn1.ObjectIdentifier) PublicKeyAlgorithm // RFC 5480, 2.1.1.1. Named Curve // -// secp224r1 OBJECT IDENTIFIER ::= { -// iso(1) identified-organization(3) certicom(132) curve(0) 33 } +// secp224r1 OBJECT IDENTIFIER ::= { +// iso(1) identified-organization(3) certicom(132) curve(0) 33 } // -// secp256r1 OBJECT IDENTIFIER ::= { -// iso(1) member-body(2) us(840) ansi-X9-62(10045) curves(3) -// prime(1) 7 } +// secp256r1 OBJECT IDENTIFIER ::= { +// iso(1) member-body(2) us(840) ansi-X9-62(10045) curves(3) +// prime(1) 7 } // -// secp384r1 OBJECT IDENTIFIER ::= { -// iso(1) identified-organization(3) certicom(132) curve(0) 34 } +// secp384r1 OBJECT IDENTIFIER ::= { +// iso(1) identified-organization(3) certicom(132) curve(0) 34 } // -// secp521r1 OBJECT IDENTIFIER ::= { -// iso(1) identified-organization(3) certicom(132) curve(0) 35 } +// secp521r1 OBJECT IDENTIFIER ::= { +// iso(1) identified-organization(3) certicom(132) curve(0) 35 } // // NB: secp256r1 is equivalent to prime256v1 var ( @@ -954,7 +956,7 @@ type distributionPointName struct { RelativeName pkix.RDNSequence `asn1:"optional,tag:1"` } -func parsePublicKey(algo PublicKeyAlgorithm, keyData *publicKeyInfo) (interface{}, error) { +func parsePublicKey(algo PublicKeyAlgorithm, keyData *publicKeyInfo, needsReencode *bool) (interface{}, error) { asn1Data := keyData.PublicKey.RightAlign() switch algo { case RSA: @@ -977,7 +979,18 @@ func parsePublicKey(algo PublicKeyAlgorithm, keyData *publicKeyInfo) (interface{ } if p.N.Sign() <= 0 { - return nil, errors.New("x509: RSA modulus is not a positive number") + // CHADRON: Android does not care, re-encode to positive + // return nil, errors.New("x509: RSA modulus is not a positive number") + if needsReencode != nil { + *needsReencode = true + } + p.N.Neg(p.N) + p.N.Sub(p.N, big.NewInt(1)) + bytes := p.N.Bytes() + for i := range bytes { + bytes[i] = ^bytes[i] + } + p.N.SetBytes(bytes) } if p.E <= 0 { return nil, errors.New("x509: RSA public exponent is not a positive number") @@ -1118,11 +1131,19 @@ func parseCertificate(in *certificate) (*Certificate, error) { out.PublicKeyAlgorithm = getPublicKeyAlgorithmFromOID(in.TBSCertificate.PublicKey.Algorithm.Algorithm) var err error - out.PublicKey, err = parsePublicKey(out.PublicKeyAlgorithm, &in.TBSCertificate.PublicKey) + var needsReencode bool + out.PublicKey, err = parsePublicKey(out.PublicKeyAlgorithm, &in.TBSCertificate.PublicKey, &needsReencode) if err != nil { return nil, err } + if needsReencode { + out.RawSubjectPublicKeyInfo, err = MarshalPKIXPublicKey(out.PublicKey) + if err != nil { + return nil, err + } + } + out.Version = in.TBSCertificate.Version + 1 out.SerialNumber = in.TBSCertificate.SerialNumber @@ -2317,7 +2338,7 @@ func parseCertificateRequest(in *certificateRequest) (*CertificateRequest, error } var err error - out.PublicKey, err = parsePublicKey(out.PublicKeyAlgorithm, &in.TBSCSR.PublicKey) + out.PublicKey, err = parsePublicKey(out.PublicKeyAlgorithm, &in.TBSCSR.PublicKey, nil) if err != nil { return nil, err } diff --git a/runtests.sh b/runtests.sh index 4172600..f9252a0 100755 --- a/runtests.sh +++ b/runtests.sh @@ -2,7 +2,7 @@ set -eux if ! [ -d "apksig_for_tests" ]; then - git clone --depth=1 -b android-s-preview-1 https://android.googlesource.com/platform/tools/apksig apksig_for_tests + git clone --depth=1 -b android14-s1-release https://android.googlesource.com/platform/tools/apksig apksig_for_tests else echo "Using cached apksig_for_test directory" fi diff --git a/schemev1.go b/schemev1.go index 2648701..fa4a8e3 100644 --- a/schemev1.go +++ b/schemev1.go @@ -430,6 +430,10 @@ func (p *schemeV1) verify(apk *apkparser.ZipReader, hasValidSigningBlock bool, m return fmt.Errorf("One or more of the signatures are invalid: %v", signatureErrors) } + if len(p.chain) > maxApkSigners { + return fmt.Errorf("APK Signature Scheme v1 only supports a maximum of %d signers, found %d", maxApkSigners, len(p.chain)) + } + return p.verifyMainManifest(apk, minSdkVersion, maxSdkVersion) } diff --git a/signingblock/schemev2.go b/signingblock/schemev2.go index 7f11b51..7f94127 100644 --- a/signingblock/schemev2.go +++ b/signingblock/schemev2.go @@ -3,7 +3,6 @@ package signingblock import ( "bytes" "encoding/binary" - "github.com/avast/apkverifier/apilevel" ) @@ -34,6 +33,11 @@ func (s *schemeV2) parseSigners(block *bytes.Buffer, contentDigests map[contentD s.verifySigner(signer, contentDigests, result) } + + if len(result.Certs) > maxApkSigners { + result.addError("APK Signature Scheme V2 only supports a maximum of %d signers, found %d", + maxApkSigners, len(result.Certs)) + } } func (s *schemeV2) finalizeResult(minSdkVersion, maxSdkVersion int32, result *VerificationResult) { @@ -131,14 +135,12 @@ func (s *schemeV2) verifySigner(signerBlock *bytes.Buffer, contentDigests map[co return } - switch strippedSchemeId { - case schemeIdV3: - if result.ExtraBlocks[blockIdSchemeV3] == nil { - result.addError("this apk was signed with v3 signing scheme, but it was stripped, downgrade attack?") - return - } - default: + strippedBlockId, prs := schemeIdToBlockId[strippedSchemeId] + if !prs { result.addError("unknown stripped scheme id: %d", strippedSchemeId) + } else if result.ExtraBlocks[strippedBlockId] == nil { + result.addError("this apk was signed with %d signing scheme, but it was stripped, downgrade attack?", strippedSchemeId) + return } default: result.addWarning("unknown additional attribute id 0x%x", uint32(id)) diff --git a/signingblock/schemev3.go b/signingblock/schemev3.go index ff7c4db..f53f004 100644 --- a/signingblock/schemev3.go +++ b/signingblock/schemev3.go @@ -15,14 +15,18 @@ import ( const ( attrV3ProofOfRotation = 0x3ba06f8c + attrV3MinSdkVersion = 0x559f8b02 + attrV3RotationOnDev = 0xc2a6b3ba lineageVersionFirst = 1 ) type schemeV3 struct { + actingAsV31 bool minSdkVersion, maxSdkVersion int32 sourceStampBlock []byte signers []*schemeV3SignerInfo + rotationMinSdkVersion int32 } type schemeV3SignerInfo struct { @@ -58,6 +62,13 @@ func (s *schemeV3) finalizeResult(requestedMinSdkVersion, requestedMaxSdkVersion requestedMinSdkVersion = apilevel.V9_0_Pie } + if s.actingAsV31 { + // V3.1 supports targeting an SDK version later than that of the initial release + // in which it is supported; allow any range for V3.1 as long as V3.0 covers the + // rest of the range. + requestedMinSdkVersion = requestedMaxSdkVersion + } + sort.Slice(s.signers, func(i, j int) bool { return s.signers[i].minSdkVersion < s.signers[j].minSdkVersion }) @@ -86,20 +97,20 @@ func (s *schemeV3) finalizeResult(requestedMinSdkVersion, requestedMaxSdkVersion } } - if firstMin > requestedMinSdkVersion || lastMax < requestedMaxSdkVersion { - msg := fmt.Sprintf("missing sdk versions, supports only <%d;%d>, got range (%d;%d)", firstMin, lastMax, requestedMinSdkVersion, requestedMaxSdkVersion) - if lastMax == apilevel.V12_1_S_V2 && result.ExtraBlocks[blockIdSchemeV31] != nil { - result.addWarning("%s, this would be an error on apksigner older than Android 13", msg) - } else { - result.addError(msg) - } + if s.rotationMinSdkVersion != 0 { + requestedMaxSdkVersion = s.rotationMinSdkVersion - 1 + } + if firstMin > requestedMinSdkVersion || lastMax < requestedMaxSdkVersion { + result.addError("missing sdk versions, supports only <%d;%d>, got range (%d;%d)", firstMin, lastMax, requestedMinSdkVersion, requestedMaxSdkVersion) } - var err error - result.SigningLineage, err = s.consolidateLineages(lineages) - if err != nil { - result.addError(err.Error()) + if result.SigningLineage == nil { + var err error + result.SigningLineage, err = s.consolidateLineages(lineages) + if err != nil { + result.addError(err.Error()) + } } } @@ -206,6 +217,8 @@ func (s *schemeV3) verifySigner(signerBlock *bytes.Buffer, contentDigests map[co return } + attrV3MinSdkVersionFound := false + additionalAttributeCount := 0 for additionalAttributes.Len() > 0 { additionalAttributeCount++ @@ -246,10 +259,42 @@ func (s *schemeV3) verifySigner(signerBlock *bytes.Buffer, contentDigests map[co result.addError("sub lineage cert mismatch") } } + case attrV3MinSdkVersion: + attrV3MinSdkVersionFound = true + if apilevel.SupportsSigV31(s.maxSdkVersion) { + var currentAttrMinSdk int32 + if err := binary.Read(attribute, binary.LittleEndian, ¤tAttrMinSdk); err != nil { + result.addError("failed to read attrRotationMinSdkVersion: %s", err.Error()) + return + } + + if s.rotationMinSdkVersion != 0 { + if s.rotationMinSdkVersion != currentAttrMinSdk { + result.addError("The v3 signer indicates key rotation should be supported starting from SDK version %d, but the v3.1 block targets %d for rotation", + s.rotationMinSdkVersion, currentAttrMinSdk) + } + } else { + result.addError("The v3 signer indicates key rotation should be supported starting from SDK version %d, but a v3.1 block was not found", currentAttrMinSdk) + } + } else { + result.addWarning("unknown additional attribute id 0x%x", id) + } + case attrV3RotationOnDev: + // This attribute should only be used by a v3.1 signer to indicate rotation + // is targeting the development release that is using the SDK version of the + // previously released platform version. + if !s.actingAsV31 { + result.addWarning("The rotation-targets-dev-release attribute is only supported on v3.1 signers; this attribute will be ignored by the platform in a v3.0 signer") + } default: - result.addWarning("unknown additional attribute id 0x%x", uint32(id)) + result.addWarning("unknown additional attribute id 0x%x", id) } } + + if s.rotationMinSdkVersion != 0 && !attrV3MinSdkVersionFound { + result.addWarning("APK supports key rotation starting from SDK version %d, but the v3 signer does not "+ + "contain the attribute to detect if this signature is stripped", s.rotationMinSdkVersion) + } } func (s *schemeV3) readSigningCertificateLineage(lineageSlice *bytes.Buffer) (V3LineageSigningCertificateNodeList, error) { diff --git a/signingblock/schemev31.go b/signingblock/schemev31.go new file mode 100644 index 0000000..dd601e9 --- /dev/null +++ b/signingblock/schemev31.go @@ -0,0 +1,60 @@ +package signingblock + +import ( + "bytes" + "github.com/avast/apkverifier/apilevel" + "math" +) + +type schemeV31 struct { + backendV3 schemeV3 + backendV31 schemeV3 +} + +func (s *schemeV31) parseSigners(block *bytes.Buffer, contentDigests map[contentDigest][]byte, result *VerificationResult) { + v31ContentDigest := make(map[contentDigest][]byte) + s.backendV31.parseSigners(block, v31ContentDigest, result) + + s.backendV3.rotationMinSdkVersion = math.MaxInt32 + for _, signer := range s.backendV31.signers { + if signer.minSdkVersion < s.backendV3.rotationMinSdkVersion { + s.backendV3.rotationMinSdkVersion = signer.minSdkVersion + } + } + if s.backendV3.rotationMinSdkVersion == math.MaxInt32 { + s.backendV3.rotationMinSdkVersion = 0 + } + + v3Block := result.ExtraBlocks[blockIdSchemeV3] + if v3Block == nil { + result.addError("The APK contains a v3.1 signing block without a v3.0 base block") + for k, v := range v31ContentDigest { + contentDigests[k] = v + } + return + } + + if result.ExtraResults == nil { + result.ExtraResults = make(map[int]*VerificationResult) + } + v3result := &VerificationResult{ + SchemeId: schemeIdV3, + } + result.ExtraResults[schemeIdV3] = v3result + + s.backendV3.parseSigners(bytes.NewBuffer(v3Block), contentDigests, v3result) +} + +func (s *schemeV31) finalizeResult(requestedMinSdkVersion, requestedMaxSdkVersion int32, result *VerificationResult) { + v31minSdk := requestedMinSdkVersion + if v31minSdk < apilevel.V13_0_TIRAMISU { + v31minSdk = apilevel.V13_0_TIRAMISU + } + s.backendV31.finalizeResult(v31minSdk, requestedMaxSdkVersion, result) + + if v3Result := result.ExtraResults[schemeIdV3]; v3Result != nil { + s.backendV3.finalizeResult(requestedMinSdkVersion, requestedMaxSdkVersion, v3Result) + result.Warnings = append(result.Warnings, v3Result.Warnings...) + result.Errors = append(result.Errors, v3Result.Errors...) + } +} diff --git a/signingblock/signer.go b/signingblock/signer.go index c4eb613..200d252 100644 --- a/signingblock/signer.go +++ b/signingblock/signer.go @@ -119,6 +119,7 @@ func (algo SignatureAlgorithm) getMinSdkVersion() int32 { /* const ( + SigRsaPssWithSha256 SignatureAlgorithm = 0x0101 SigRsaPssWithSha512 = 0x0102 SigRsaPkcs1V15WithSha256 = 0x0103 @@ -129,6 +130,7 @@ const ( SigVerityRsaPkcs1V15WithSha256 = 0x0421 SigVerityEcdsaWithSha256 = 0x0423 SigVerityDsaWithSha256 = 0x425 + ) */ func (algo SignatureAlgorithm) getMinSdkVersionJca() int32 { diff --git a/signingblock/signingblock.go b/signingblock/signingblock.go index 434ddb2..b2ece2e 100644 --- a/signingblock/signingblock.go +++ b/signingblock/signingblock.go @@ -23,15 +23,15 @@ import ( type BlockId uint32 const ( - // Dependencies metadata generated by Gradle and encrypted by Google Play. + // BlockIdDependencyMetadata Dependencies metadata generated by Gradle and encrypted by Google Play. // "...The data is compressed, encrypted by a Google Play signing key..." // https://developer.android.com/studio/releases/gradle-plugin#dependency-metadata BlockIdDependencyMetadata BlockId = 0x504b4453 - // JSON with some metadata, used by Chinese company Meituan + // BlockIdMeituanMetadata JSON with some metadata, used by Chinese company Meituan BlockIdMeituanMetadata BlockId = 0x71777777 - // Older SourceStamp implementation, you should not encounter this ID + // BlockIdSourceStampV1 Older SourceStamp implementation, you should not encounter this ID // https://android.googlesource.com/platform/frameworks/base/+/549ce7a482ed4fe170ca445324fb38c447030404%5E%21/#F0 BlockIdSourceStampV1 BlockId = 0x2b09189e @@ -60,7 +60,7 @@ func (b BlockId) String() string { case blockIdSchemeV3: return "SchemeV3Signature" case blockIdSchemeV31: - return "SchemeV31Signature" + return "SchemeV3.1Signature" case blockIdFrosting: return "PlayFrosting" default: @@ -84,11 +84,20 @@ const ( maxChunkSize = 1024 * 1024 - schemeIdV1 = 1 - schemeIdV2 = 2 - schemeIdV3 = 3 + maxApkSigners = 10 + + schemeIdV1 = 1 + schemeIdV2 = 2 + schemeIdV3 = 3 + schemeIdV31 = 31 ) +var schemeIdToBlockId = map[int32]BlockId{ + schemeIdV2: blockIdSchemeV2, + schemeIdV3: blockIdSchemeV3, + schemeIdV31: blockIdSchemeV31, +} + var ( errNoSigningBlockSignature = errors.New("This apk does not have signing block signature") errEocdNotFound = errors.New("EOCD record not found.") @@ -163,7 +172,7 @@ func VerifySigningBlockReaderWithZip(r io.ReadSeeker, minSdkVersion, maxSdkVersi contentDigests := s.pickAndVerify(blocks, minSdkVersion, maxSdkVersion, res) // On levels < 28, we can fallback to v2 - v3 is not required to be valid. // 1e2ab0af91dce1be16525d9d6d6e6d645788ea627edc64cb9cd379b35e01f53f - if res.ContainsErrors() && res.SchemeId == schemeIdV3 && minSdkVersion < apilevel.V9_0_Pie && blocks[blockIdSchemeV2] != nil { + if res.ContainsErrors() && res.SchemeId >= schemeIdV3 && minSdkVersion < apilevel.V9_0_Pie && blocks[blockIdSchemeV2] != nil { if res.ExtraBlocks == nil { res.ExtraBlocks = make(map[BlockId][]byte) } @@ -348,9 +357,22 @@ func (s *signingBlock) isZip64() bool { } func (s *signingBlock) pickScheme(blocks map[BlockId][]byte, minSdkVersion, maxSdkVersion int32) (schemeId int, scheme signatureBlockScheme, block []byte, err error) { - if block = blocks[blockIdSchemeV3]; block != nil { + if block = blocks[blockIdSchemeV31]; block != nil { + schemeId = schemeIdV31 + scheme = &schemeV31{ + backendV31: schemeV3{ + actingAsV31: true, + minSdkVersion: minSdkVersion, + maxSdkVersion: maxSdkVersion, + }, + backendV3: schemeV3{ + minSdkVersion: minSdkVersion, + maxSdkVersion: maxSdkVersion, + }, + } + } else if block = blocks[blockIdSchemeV3]; block != nil { schemeId = schemeIdV3 - scheme = &schemeV3{} + scheme = &schemeV3{minSdkVersion: minSdkVersion, maxSdkVersion: maxSdkVersion} } else if block = blocks[blockIdSchemeV2]; block != nil { schemeId = schemeIdV2 scheme = &schemeV2{minSdkVersion, maxSdkVersion} @@ -468,14 +490,21 @@ func (s *signingBlock) findSignatureBlocks(maxSdkVersion int32, res *Verificatio } switch bid := BlockId(id); bid { - case blockIdSchemeV2, blockIdSchemeV3: + case blockIdSchemeV2, blockIdSchemeV3, blockIdSchemeV31: block := make([]byte, entryLen-4) if _, err = pairs.Read(block); err != nil { return } - if (bid == blockIdSchemeV2 && apilevel.SupportsSigV2(maxSdkVersion)) || (bid == blockIdSchemeV3 && apilevel.SupportsSigV3(maxSdkVersion)) { + + isSupported := (bid == blockIdSchemeV2 && apilevel.SupportsSigV2(maxSdkVersion)) || + (bid == blockIdSchemeV3 && apilevel.SupportsSigV3(maxSdkVersion)) || + (bid == blockIdSchemeV31 && apilevel.SupportsSigV31(maxSdkVersion)) + + if isSupported { blocks[bid] = block - } else { + } + + if !isSupported || (bid == blockIdSchemeV3 && apilevel.SupportsSigV31(maxSdkVersion)) { if res.ExtraBlocks == nil { res.ExtraBlocks = make(map[BlockId][]byte) } diff --git a/signingblock/sourcestamp.go b/signingblock/sourcestamp.go index 411902c..9e538c1 100644 --- a/signingblock/sourcestamp.go +++ b/signingblock/sourcestamp.go @@ -10,6 +10,7 @@ import ( "math" "sort" "strings" + "time" "github.com/avast/apkparser" @@ -18,6 +19,7 @@ import ( const ( sourceStampAttrProofOfRotation = 0x9d6303f7 + sourceStampAttrTime = 0xe43c5946 sourceStampZipEntryName = "stamp-cert-sha256" sourceStampHashSizeLimit = 64 * 1024 @@ -32,10 +34,11 @@ type SourceStampLineageNode struct { } type SourceStampResult struct { - Cert *x509.Certificate - Lineage []*SourceStampLineageNode - Errors []error - Warnings []string + Cert *x509.Certificate + SigningTime time.Time + Lineage []*SourceStampLineageNode + Errors []error + Warnings []string } type SourceStampCertMismatchError struct { @@ -121,6 +124,11 @@ func (v *sourceStampVerifier) VerifySourceV2Stamp(zip *apkparser.ZipReader, bloc return } + neededSchemeId := v.verifiedSchemeId + if neededSchemeId == schemeIdV31 { + neededSchemeId = schemeIdV3 + } + var signaturesBlock *bytes.Buffer for signedSignatureSchemes.Len() != 0 { schemeBuf, err := getLenghtPrefixedSlice(signedSignatureSchemes) @@ -141,13 +149,13 @@ func (v *sourceStampVerifier) VerifySourceV2Stamp(zip *apkparser.ZipReader, bloc return } - if schemeId == v.verifiedSchemeId { + if schemeId == neededSchemeId { signaturesBlock = apkDigestSignatures } } if signaturesBlock == nil { - v.addError("No source stamp signature for scheme %d", v.verifiedSchemeId) + v.addError("No source stamp signature for scheme %d", neededSchemeId) return } @@ -347,6 +355,18 @@ func (v *sourceStampVerifier) parseStampAttributes(attributesData *bytes.Buffer) v.addError("lineage certificate mismatch") return false } + case sourceStampAttrTime: + var timestamp int64 + if err := binary.Read(attr, binary.LittleEndian, ×tamp); err != nil { + v.addError("failed to parse attribute timestamp: %s", err.Error()) + return false + } + + if timestamp <= 0 { + v.addError("Invalid timestamp %d in source stamp", timestamp) + } else { + v.res.SigningTime = time.Unix(timestamp, 0) + } default: v.addWarning("Unknown attribute 0x%08x is present.", id) } diff --git a/signingblock/verificationresult.go b/signingblock/verificationresult.go index a146f6f..e62b884 100644 --- a/signingblock/verificationresult.go +++ b/signingblock/verificationresult.go @@ -10,6 +10,9 @@ type VerificationResult struct { SchemeId int SigningLineage *V3SigningLineage + // When APK is signed with v3.1, the v3 result is stored here. Any v3 errors are lifted to the main Warnings/errors though + ExtraResults map[int]*VerificationResult + Frosting *FrostingResult SourceStamp *SourceStampResult diff --git a/sourcestamp_test.go b/sourcestamp_test.go index e7cd47d..280fb5c 100644 --- a/sourcestamp_test.go +++ b/sourcestamp_test.go @@ -13,7 +13,7 @@ import ( "github.com/avast/apkverifier/apilevel" ) -// From https://android.googlesource.com/platform/tools/apksig 907b962a6702ca25a28ed54b14964b5b713aeedb +// From https://android.googlesource.com/platform/tools/apksig c5b2567d87833f347cd578acc0262501c7eae423 const ( RSA_2048_CERT_SHA256_DIGEST = "fb5dbd3c669af9fc236c6991e6387b7f11ff0590997f22d0f5c74ff40e04fca8" @@ -76,6 +76,46 @@ func TestSourceStampV2OnlySignatureValidStamp(t *testing.T) { func TestSourceStampV3OnlySignatureValidStamp(t *testing.T) { r := stampAssertVerifiedSdk(t, "v3-only-with-stamp.apk", apilevel.V9_0_Pie, apilevel.V9_0_Pie) stampAssertCert(t, r, 3, EC_P256_CERT_SHA256_DIGEST) + + if !r.SigningBlockResult.SourceStamp.SigningTime.IsZero() { + t.Fatalf("SourceStamp has Non-zero time %v!", r.SigningBlockResult.SourceStamp.SigningTime) + } +} + +func TestSourceStampValidTimestamp(t *testing.T) { + r := stampAssertVerifiedSdk(t, "stamp-valid-timestamp-value.apk", apilevel.V_AnyMin, apilevel.V_AnyMax) + if r.SigningBlockResult.SourceStamp.SigningTime.Unix() != 1644886584 { + t.Fatalf("SourceStamp has wrong time, expected 1644886584 got %d!", + r.SigningBlockResult.SourceStamp.SigningTime.Unix()) + } +} + +func TestSourceStampValidTimestampLargeBuffer(t *testing.T) { + r := stampAssertVerifiedSdk(t, "stamp-valid-timestamp-16-byte-buffer.apk", apilevel.V_AnyMin, apilevel.V_AnyMax) + if r.SigningBlockResult.SourceStamp.SigningTime.Unix() != 1645126786 { + t.Fatalf("SourceStamp has wrong time, expected 1645126786 got %d!", + r.SigningBlockResult.SourceStamp.SigningTime.Unix()) + } +} + +func TestSourceStampInvalidTimestampValueEqualsZero(t *testing.T) { + stampAssertFailureSdk(t, "stamp-invalid-timestamp-value-zero.apk", apilevel.V_AnyMin, apilevel.V_AnyMax, + "Invalid timestamp 0 in source stamp") +} + +func TestSourceStampInvalidTimestampValueLessThanZero(t *testing.T) { + stampAssertFailureSdk(t, "stamp-invalid-timestamp-value-less-than-zero.apk", apilevel.V_AnyMin, apilevel.V_AnyMax, + "Invalid timestamp") +} + +func TestSourceStampInvalidTimestampZeroInFirst8BytesOfBuffer(t *testing.T) { + stampAssertFailureSdk(t, "stamp-timestamp-in-last-8-of-16-byte-buffer.apk", apilevel.V_AnyMin, apilevel.V_AnyMax, + "Invalid timestamp") +} + +func TestSourceStampModifiedTimestampValue(t *testing.T) { + stampAssertFailureSdk(t, "stamp-valid-timestamp-value-modified.apk", apilevel.V_AnyMin, apilevel.V_AnyMax, + "failed to verify signature") } func TestSourceStampApkHashMismatchV1Scheme(t *testing.T) {