| /* |
| * Copyright (C) 2018 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.util.apk; |
| |
| import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256; |
| import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm; |
| import static android.util.apk.ApkSigningBlockUtils.getContentDigestAlgorithmJcaDigestAlgorithm; |
| import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice; |
| import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContentDigestAlgorithm; |
| import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaKeyAlgorithm; |
| import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm; |
| import static android.util.apk.ApkSigningBlockUtils.isSupportedSignatureAlgorithm; |
| import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray; |
| |
| import android.os.Build; |
| import android.util.ArrayMap; |
| import android.util.Pair; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.IOException; |
| import java.io.RandomAccessFile; |
| import java.nio.BufferUnderflowException; |
| import java.nio.ByteBuffer; |
| import java.security.DigestException; |
| import java.security.InvalidAlgorithmParameterException; |
| import java.security.InvalidKeyException; |
| import java.security.KeyFactory; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.PublicKey; |
| import java.security.Signature; |
| import java.security.SignatureException; |
| import java.security.cert.CertificateEncodingException; |
| import java.security.cert.CertificateException; |
| import java.security.cert.CertificateFactory; |
| import java.security.cert.X509Certificate; |
| import java.security.spec.AlgorithmParameterSpec; |
| import java.security.spec.InvalidKeySpecException; |
| import java.security.spec.X509EncodedKeySpec; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * APK Signature Scheme v3 verifier. |
| * |
| * @hide for internal use only. |
| */ |
| public class ApkSignatureSchemeV3Verifier { |
| |
| /** |
| * ID of this signature scheme as used in X-Android-APK-Signed header used in JAR signing. |
| */ |
| public static final int SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID = 3; |
| |
| private static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0; |
| |
| /** |
| * Returns {@code true} if the provided APK contains an APK Signature Scheme V3 signature. |
| * |
| * <p><b>NOTE: This method does not verify the signature.</b> |
| */ |
| public static boolean hasSignature(String apkFile) throws IOException { |
| try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) { |
| findSignature(apk); |
| return true; |
| } catch (SignatureNotFoundException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Verifies APK Signature Scheme v3 signatures of the provided APK and returns the certificates |
| * associated with each signer. |
| * |
| * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3. |
| * @throws SecurityException if the APK Signature Scheme v3 signature of this APK does not |
| * verify. |
| * @throws IOException if an I/O error occurs while reading the APK file. |
| */ |
| public static VerifiedSigner verify(String apkFile) |
| throws SignatureNotFoundException, SecurityException, IOException { |
| return verify(apkFile, true); |
| } |
| |
| /** |
| * Returns the certificates associated with each signer for the given APK without verification. |
| * This method is dangerous and should not be used, unless the caller is absolutely certain the |
| * APK is trusted. Specifically, verification is only done for the APK Signature Scheme v3 |
| * Block while gathering signer information. The APK contents are not verified. |
| * |
| * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3. |
| * @throws IOException if an I/O error occurs while reading the APK file. |
| */ |
| public static VerifiedSigner unsafeGetCertsWithoutVerification(String apkFile) |
| throws SignatureNotFoundException, SecurityException, IOException { |
| return verify(apkFile, false); |
| } |
| |
| private static VerifiedSigner verify(String apkFile, boolean verifyIntegrity) |
| throws SignatureNotFoundException, SecurityException, IOException { |
| try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) { |
| return verify(apk, verifyIntegrity); |
| } |
| } |
| |
| /** |
| * Verifies APK Signature Scheme v3 signatures of the provided APK and returns the certificates |
| * associated with each signer. |
| * |
| * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3. |
| * @throws SecurityException if an APK Signature Scheme v3 signature of this APK does not |
| * verify. |
| * @throws IOException if an I/O error occurs while reading the APK file. |
| */ |
| private static VerifiedSigner verify(RandomAccessFile apk, boolean verifyIntegrity) |
| throws SignatureNotFoundException, SecurityException, IOException { |
| SignatureInfo signatureInfo = findSignature(apk); |
| return verify(apk, signatureInfo, verifyIntegrity); |
| } |
| |
| /** |
| * Returns the APK Signature Scheme v3 block contained in the provided APK file and the |
| * additional information relevant for verifying the block against the file. |
| * |
| * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3. |
| * @throws IOException if an I/O error occurs while reading the APK file. |
| */ |
| private static SignatureInfo findSignature(RandomAccessFile apk) |
| throws IOException, SignatureNotFoundException { |
| return ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V3_BLOCK_ID); |
| } |
| |
| /** |
| * Verifies the contents of the provided APK file against the provided APK Signature Scheme v3 |
| * Block. |
| * |
| * @param signatureInfo APK Signature Scheme v3 Block and information relevant for verifying it |
| * against the APK file. |
| */ |
| private static VerifiedSigner verify( |
| RandomAccessFile apk, |
| SignatureInfo signatureInfo, |
| boolean doVerifyIntegrity) throws SecurityException, IOException { |
| int signerCount = 0; |
| Map<Integer, byte[]> contentDigests = new ArrayMap<>(); |
| VerifiedSigner result = null; |
| CertificateFactory certFactory; |
| try { |
| certFactory = CertificateFactory.getInstance("X.509"); |
| } catch (CertificateException e) { |
| throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); |
| } |
| ByteBuffer signers; |
| try { |
| signers = getLengthPrefixedSlice(signatureInfo.signatureBlock); |
| } catch (IOException e) { |
| throw new SecurityException("Failed to read list of signers", e); |
| } |
| while (signers.hasRemaining()) { |
| try { |
| ByteBuffer signer = getLengthPrefixedSlice(signers); |
| result = verifySigner(signer, contentDigests, certFactory); |
| signerCount++; |
| } catch (PlatformNotSupportedException e) { |
| // this signer is for a different platform, ignore it. |
| continue; |
| } catch (IOException | BufferUnderflowException | SecurityException e) { |
| throw new SecurityException( |
| "Failed to parse/verify signer #" + signerCount + " block", |
| e); |
| } |
| } |
| |
| if (signerCount < 1 || result == null) { |
| throw new SecurityException("No signers found"); |
| } |
| |
| if (signerCount != 1) { |
| throw new SecurityException("APK Signature Scheme V3 only supports one signer: " |
| + "multiple signers found."); |
| } |
| |
| if (contentDigests.isEmpty()) { |
| throw new SecurityException("No content digests found"); |
| } |
| |
| if (doVerifyIntegrity) { |
| ApkSigningBlockUtils.verifyIntegrity(contentDigests, apk, signatureInfo); |
| } |
| |
| if (contentDigests.containsKey(CONTENT_DIGEST_VERITY_CHUNKED_SHA256)) { |
| byte[] verityDigest = contentDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256); |
| result.verityRootHash = ApkSigningBlockUtils.parseVerityDigestAndVerifySourceLength( |
| verityDigest, apk.length(), signatureInfo); |
| } |
| |
| return result; |
| } |
| |
| private static VerifiedSigner verifySigner( |
| ByteBuffer signerBlock, |
| Map<Integer, byte[]> contentDigests, |
| CertificateFactory certFactory) |
| throws SecurityException, IOException, PlatformNotSupportedException { |
| ByteBuffer signedData = getLengthPrefixedSlice(signerBlock); |
| int minSdkVersion = signerBlock.getInt(); |
| int maxSdkVersion = signerBlock.getInt(); |
| |
| if (Build.VERSION.SDK_INT < minSdkVersion || Build.VERSION.SDK_INT > maxSdkVersion) { |
| // this signature isn't meant to be used with this platform, skip it. |
| throw new PlatformNotSupportedException( |
| "Signer not supported by this platform " |
| + "version. This platform: " + Build.VERSION.SDK_INT |
| + ", signer minSdkVersion: " + minSdkVersion |
| + ", maxSdkVersion: " + maxSdkVersion); |
| } |
| |
| ByteBuffer signatures = getLengthPrefixedSlice(signerBlock); |
| byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock); |
| |
| int signatureCount = 0; |
| int bestSigAlgorithm = -1; |
| byte[] bestSigAlgorithmSignatureBytes = null; |
| List<Integer> signaturesSigAlgorithms = new ArrayList<>(); |
| while (signatures.hasRemaining()) { |
| signatureCount++; |
| try { |
| ByteBuffer signature = getLengthPrefixedSlice(signatures); |
| if (signature.remaining() < 8) { |
| throw new SecurityException("Signature record too short"); |
| } |
| int sigAlgorithm = signature.getInt(); |
| signaturesSigAlgorithms.add(sigAlgorithm); |
| if (!isSupportedSignatureAlgorithm(sigAlgorithm)) { |
| continue; |
| } |
| if ((bestSigAlgorithm == -1) |
| || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) { |
| bestSigAlgorithm = sigAlgorithm; |
| bestSigAlgorithmSignatureBytes = readLengthPrefixedByteArray(signature); |
| } |
| } catch (IOException | BufferUnderflowException e) { |
| throw new SecurityException( |
| "Failed to parse signature record #" + signatureCount, |
| e); |
| } |
| } |
| if (bestSigAlgorithm == -1) { |
| if (signatureCount == 0) { |
| throw new SecurityException("No signatures found"); |
| } else { |
| throw new SecurityException("No supported signatures found"); |
| } |
| } |
| |
| String keyAlgorithm = getSignatureAlgorithmJcaKeyAlgorithm(bestSigAlgorithm); |
| Pair<String, ? extends AlgorithmParameterSpec> signatureAlgorithmParams = |
| getSignatureAlgorithmJcaSignatureAlgorithm(bestSigAlgorithm); |
| String jcaSignatureAlgorithm = signatureAlgorithmParams.first; |
| AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second; |
| boolean sigVerified; |
| try { |
| PublicKey publicKey = |
| KeyFactory.getInstance(keyAlgorithm) |
| .generatePublic(new X509EncodedKeySpec(publicKeyBytes)); |
| Signature sig = Signature.getInstance(jcaSignatureAlgorithm); |
| sig.initVerify(publicKey); |
| if (jcaSignatureAlgorithmParams != null) { |
| sig.setParameter(jcaSignatureAlgorithmParams); |
| } |
| sig.update(signedData); |
| sigVerified = sig.verify(bestSigAlgorithmSignatureBytes); |
| } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException |
| | InvalidAlgorithmParameterException | SignatureException e) { |
| throw new SecurityException( |
| "Failed to verify " + jcaSignatureAlgorithm + " signature", e); |
| } |
| if (!sigVerified) { |
| throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify"); |
| } |
| |
| // Signature over signedData has verified. |
| |
| byte[] contentDigest = null; |
| signedData.clear(); |
| ByteBuffer digests = getLengthPrefixedSlice(signedData); |
| List<Integer> digestsSigAlgorithms = new ArrayList<>(); |
| int digestCount = 0; |
| while (digests.hasRemaining()) { |
| digestCount++; |
| try { |
| ByteBuffer digest = getLengthPrefixedSlice(digests); |
| if (digest.remaining() < 8) { |
| throw new IOException("Record too short"); |
| } |
| int sigAlgorithm = digest.getInt(); |
| digestsSigAlgorithms.add(sigAlgorithm); |
| if (sigAlgorithm == bestSigAlgorithm) { |
| contentDigest = readLengthPrefixedByteArray(digest); |
| } |
| } catch (IOException | BufferUnderflowException e) { |
| throw new IOException("Failed to parse digest record #" + digestCount, e); |
| } |
| } |
| |
| if (!signaturesSigAlgorithms.equals(digestsSigAlgorithms)) { |
| throw new SecurityException( |
| "Signature algorithms don't match between digests and signatures records"); |
| } |
| int digestAlgorithm = getSignatureAlgorithmContentDigestAlgorithm(bestSigAlgorithm); |
| byte[] previousSignerDigest = contentDigests.put(digestAlgorithm, contentDigest); |
| if ((previousSignerDigest != null) |
| && (!MessageDigest.isEqual(previousSignerDigest, contentDigest))) { |
| throw new SecurityException( |
| getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm) |
| + " contents digest does not match the digest specified by a preceding signer"); |
| } |
| |
| ByteBuffer certificates = getLengthPrefixedSlice(signedData); |
| List<X509Certificate> certs = new ArrayList<>(); |
| int certificateCount = 0; |
| while (certificates.hasRemaining()) { |
| certificateCount++; |
| byte[] encodedCert = readLengthPrefixedByteArray(certificates); |
| X509Certificate certificate; |
| try { |
| certificate = (X509Certificate) |
| certFactory.generateCertificate(new ByteArrayInputStream(encodedCert)); |
| } catch (CertificateException e) { |
| throw new SecurityException("Failed to decode certificate #" + certificateCount, e); |
| } |
| certificate = new VerbatimX509Certificate( |
| certificate, encodedCert); |
| certs.add(certificate); |
| } |
| |
| if (certs.isEmpty()) { |
| throw new SecurityException("No certificates listed"); |
| } |
| X509Certificate mainCertificate = certs.get(0); |
| byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded(); |
| if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) { |
| throw new SecurityException( |
| "Public key mismatch between certificate and signature record"); |
| } |
| |
| int signedMinSDK = signedData.getInt(); |
| if (signedMinSDK != minSdkVersion) { |
| throw new SecurityException( |
| "minSdkVersion mismatch between signed and unsigned in v3 signer block."); |
| } |
| |
| int signedMaxSDK = signedData.getInt(); |
| if (signedMaxSDK != maxSdkVersion) { |
| throw new SecurityException( |
| "maxSdkVersion mismatch between signed and unsigned in v3 signer block."); |
| } |
| |
| ByteBuffer additionalAttrs = getLengthPrefixedSlice(signedData); |
| return verifyAdditionalAttributes(additionalAttrs, certs, certFactory); |
| } |
| |
| private static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c; |
| |
| private static VerifiedSigner verifyAdditionalAttributes(ByteBuffer attrs, |
| List<X509Certificate> certs, CertificateFactory certFactory) throws IOException { |
| X509Certificate[] certChain = certs.toArray(new X509Certificate[certs.size()]); |
| VerifiedProofOfRotation por = null; |
| |
| while (attrs.hasRemaining()) { |
| ByteBuffer attr = getLengthPrefixedSlice(attrs); |
| if (attr.remaining() < 4) { |
| throw new IOException("Remaining buffer too short to contain additional attribute " |
| + "ID. Remaining: " + attr.remaining()); |
| } |
| int id = attr.getInt(); |
| switch(id) { |
| case PROOF_OF_ROTATION_ATTR_ID: |
| if (por != null) { |
| throw new SecurityException("Encountered multiple Proof-of-rotation records" |
| + " when verifying APK Signature Scheme v3 signature"); |
| } |
| por = verifyProofOfRotationStruct(attr, certFactory); |
| // make sure that the last certificate in the Proof-of-rotation record matches |
| // the one used to sign this APK. |
| try { |
| if (por.certs.size() > 0 |
| && !Arrays.equals(por.certs.get(por.certs.size() - 1).getEncoded(), |
| certChain[0].getEncoded())) { |
| throw new SecurityException("Terminal certificate in Proof-of-rotation" |
| + " record does not match APK signing certificate"); |
| } |
| } catch (CertificateEncodingException e) { |
| throw new SecurityException("Failed to encode certificate when comparing" |
| + " Proof-of-rotation record and signing certificate", e); |
| } |
| |
| break; |
| default: |
| // not the droid we're looking for, move along, move along. |
| break; |
| } |
| } |
| return new VerifiedSigner(certChain, por); |
| } |
| |
| private static VerifiedProofOfRotation verifyProofOfRotationStruct( |
| ByteBuffer porBuf, |
| CertificateFactory certFactory) |
| throws SecurityException, IOException { |
| int levelCount = 0; |
| int lastSigAlgorithm = -1; |
| X509Certificate lastCert = null; |
| List<X509Certificate> certs = new ArrayList<>(); |
| List<Integer> flagsList = new ArrayList<>(); |
| |
| // Proof-of-rotation struct: |
| // A uint32 version code followed by basically a singly linked list of nodes, called levels |
| // here, each of which have the following structure: |
| // * length-prefix for the entire level |
| // - length-prefixed signed data (if previous level exists) |
| // * length-prefixed X509 Certificate |
| // * uint32 signature algorithm ID describing how this signed data was signed |
| // - uint32 flags describing how to treat the cert contained in this level |
| // - uint32 signature algorithm ID to use to verify the signature of the next level. The |
| // algorithm here must match the one in the signed data section of the next level. |
| // - length-prefixed signature over the signed data in this level. The signature here |
| // is verified using the certificate from the previous level. |
| // The linking is provided by the certificate of each level signing the one of the next. |
| |
| try { |
| |
| // get the version code, but don't do anything with it: creator knew about all our flags |
| porBuf.getInt(); |
| HashSet<X509Certificate> certHistorySet = new HashSet<>(); |
| while (porBuf.hasRemaining()) { |
| levelCount++; |
| ByteBuffer level = getLengthPrefixedSlice(porBuf); |
| ByteBuffer signedData = getLengthPrefixedSlice(level); |
| int flags = level.getInt(); |
| int sigAlgorithm = level.getInt(); |
| byte[] signature = readLengthPrefixedByteArray(level); |
| |
| if (lastCert != null) { |
| // Use previous level cert to verify current level |
| Pair<String, ? extends AlgorithmParameterSpec> sigAlgParams = |
| getSignatureAlgorithmJcaSignatureAlgorithm(lastSigAlgorithm); |
| PublicKey publicKey = lastCert.getPublicKey(); |
| Signature sig = Signature.getInstance(sigAlgParams.first); |
| sig.initVerify(publicKey); |
| if (sigAlgParams.second != null) { |
| sig.setParameter(sigAlgParams.second); |
| } |
| sig.update(signedData); |
| if (!sig.verify(signature)) { |
| throw new SecurityException("Unable to verify signature of certificate #" |
| + levelCount + " using " + sigAlgParams.first + " when verifying" |
| + " Proof-of-rotation record"); |
| } |
| } |
| |
| signedData.rewind(); |
| byte[] encodedCert = readLengthPrefixedByteArray(signedData); |
| int signedSigAlgorithm = signedData.getInt(); |
| if (lastCert != null && lastSigAlgorithm != signedSigAlgorithm) { |
| throw new SecurityException("Signing algorithm ID mismatch for certificate #" |
| + levelCount + " when verifying Proof-of-rotation record"); |
| } |
| lastCert = (X509Certificate) |
| certFactory.generateCertificate(new ByteArrayInputStream(encodedCert)); |
| lastCert = new VerbatimX509Certificate(lastCert, encodedCert); |
| |
| lastSigAlgorithm = sigAlgorithm; |
| if (certHistorySet.contains(lastCert)) { |
| throw new SecurityException("Encountered duplicate entries in " |
| + "Proof-of-rotation record at certificate #" + levelCount + ". All " |
| + "signing certificates should be unique"); |
| } |
| certHistorySet.add(lastCert); |
| certs.add(lastCert); |
| flagsList.add(flags); |
| } |
| } catch (IOException | BufferUnderflowException e) { |
| throw new IOException("Failed to parse Proof-of-rotation record", e); |
| } catch (NoSuchAlgorithmException | InvalidKeyException |
| | InvalidAlgorithmParameterException | SignatureException e) { |
| throw new SecurityException( |
| "Failed to verify signature over signed data for certificate #" |
| + levelCount + " when verifying Proof-of-rotation record", e); |
| } catch (CertificateException e) { |
| throw new SecurityException("Failed to decode certificate #" + levelCount |
| + " when verifying Proof-of-rotation record", e); |
| } |
| return new VerifiedProofOfRotation(certs, flagsList); |
| } |
| |
| static byte[] getVerityRootHash(String apkPath) |
| throws IOException, SignatureNotFoundException, SecurityException { |
| try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) { |
| SignatureInfo signatureInfo = findSignature(apk); |
| VerifiedSigner vSigner = verify(apk, false); |
| return vSigner.verityRootHash; |
| } |
| } |
| |
| static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory) |
| throws IOException, SignatureNotFoundException, SecurityException, DigestException, |
| NoSuchAlgorithmException { |
| try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) { |
| SignatureInfo signatureInfo = findSignature(apk); |
| return VerityBuilder.generateApkVerity(apkPath, bufferFactory, signatureInfo); |
| } |
| } |
| |
| static byte[] generateApkVerityRootHash(String apkPath) |
| throws NoSuchAlgorithmException, DigestException, IOException, |
| SignatureNotFoundException { |
| try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) { |
| SignatureInfo signatureInfo = findSignature(apk); |
| VerifiedSigner vSigner = verify(apk, false); |
| if (vSigner.verityRootHash == null) { |
| return null; |
| } |
| return VerityBuilder.generateApkVerityRootHash( |
| apk, ByteBuffer.wrap(vSigner.verityRootHash), signatureInfo); |
| } |
| } |
| |
| /** |
| * Verified processed proof of rotation. |
| * |
| * @hide for internal use only. |
| */ |
| public static class VerifiedProofOfRotation { |
| public final List<X509Certificate> certs; |
| public final List<Integer> flagsList; |
| |
| public VerifiedProofOfRotation(List<X509Certificate> certs, List<Integer> flagsList) { |
| this.certs = certs; |
| this.flagsList = flagsList; |
| } |
| } |
| |
| /** |
| * Verified APK Signature Scheme v3 signer, including the proof of rotation structure. |
| * |
| * @hide for internal use only. |
| */ |
| public static class VerifiedSigner { |
| public final X509Certificate[] certs; |
| public final VerifiedProofOfRotation por; |
| |
| public byte[] verityRootHash; |
| |
| public VerifiedSigner(X509Certificate[] certs, VerifiedProofOfRotation por) { |
| this.certs = certs; |
| this.por = por; |
| } |
| |
| } |
| |
| private static class PlatformNotSupportedException extends Exception { |
| |
| PlatformNotSupportedException(String s) { |
| super(s); |
| } |
| } |
| } |