You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
319 lines
12 KiB
319 lines
12 KiB
/* |
|
* Copyright (C) 2014 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 com.android.verity; |
|
|
|
import java.io.ByteArrayInputStream; |
|
import java.io.IOException; |
|
import java.nio.ByteBuffer; |
|
import java.nio.ByteOrder; |
|
import java.security.PrivateKey; |
|
import java.security.PublicKey; |
|
import java.security.Security; |
|
import java.security.cert.X509Certificate; |
|
import java.security.cert.Certificate; |
|
import java.security.cert.CertificateFactory; |
|
import java.security.cert.CertificateEncodingException; |
|
import java.util.Arrays; |
|
import org.bouncycastle.asn1.ASN1Encodable; |
|
import org.bouncycastle.asn1.ASN1EncodableVector; |
|
import org.bouncycastle.asn1.ASN1Integer; |
|
import org.bouncycastle.asn1.ASN1Object; |
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier; |
|
import org.bouncycastle.asn1.ASN1OctetString; |
|
import org.bouncycastle.asn1.ASN1Primitive; |
|
import org.bouncycastle.asn1.ASN1Sequence; |
|
import org.bouncycastle.asn1.ASN1InputStream; |
|
import org.bouncycastle.asn1.DEROctetString; |
|
import org.bouncycastle.asn1.DERPrintableString; |
|
import org.bouncycastle.asn1.DERSequence; |
|
import org.bouncycastle.asn1.util.ASN1Dump; |
|
import org.bouncycastle.asn1.x509.AlgorithmIdentifier; |
|
import org.bouncycastle.jce.provider.BouncyCastleProvider; |
|
|
|
/** |
|
* AndroidVerifiedBootSignature DEFINITIONS ::= |
|
* BEGIN |
|
* formatVersion ::= INTEGER |
|
* certificate ::= Certificate |
|
* algorithmIdentifier ::= SEQUENCE { |
|
* algorithm OBJECT IDENTIFIER, |
|
* parameters ANY DEFINED BY algorithm OPTIONAL |
|
* } |
|
* authenticatedAttributes ::= SEQUENCE { |
|
* target CHARACTER STRING, |
|
* length INTEGER |
|
* } |
|
* signature ::= OCTET STRING |
|
* END |
|
*/ |
|
|
|
public class BootSignature extends ASN1Object |
|
{ |
|
private ASN1Integer formatVersion; |
|
private ASN1Encodable certificate; |
|
private AlgorithmIdentifier algorithmIdentifier; |
|
private DERPrintableString target; |
|
private ASN1Integer length; |
|
private DEROctetString signature; |
|
private PublicKey publicKey; |
|
|
|
private static final int FORMAT_VERSION = 1; |
|
|
|
/** |
|
* Initializes the object for signing an image file |
|
* @param target Target name, included in the signed data |
|
* @param length Length of the image, included in the signed data |
|
*/ |
|
public BootSignature(String target, int length) { |
|
this.formatVersion = new ASN1Integer(FORMAT_VERSION); |
|
this.target = new DERPrintableString(target); |
|
this.length = new ASN1Integer(length); |
|
} |
|
|
|
/** |
|
* Initializes the object for verifying a signed image file |
|
* @param signature Signature footer |
|
*/ |
|
public BootSignature(byte[] signature) |
|
throws Exception { |
|
ASN1InputStream stream = new ASN1InputStream(signature); |
|
ASN1Sequence sequence = (ASN1Sequence) stream.readObject(); |
|
|
|
formatVersion = (ASN1Integer) sequence.getObjectAt(0); |
|
if (formatVersion.getValue().intValue() != FORMAT_VERSION) { |
|
throw new IllegalArgumentException("Unsupported format version"); |
|
} |
|
|
|
certificate = sequence.getObjectAt(1); |
|
byte[] encoded = ((ASN1Object) certificate).getEncoded(); |
|
ByteArrayInputStream bis = new ByteArrayInputStream(encoded); |
|
|
|
CertificateFactory cf = CertificateFactory.getInstance("X.509"); |
|
X509Certificate c = (X509Certificate) cf.generateCertificate(bis); |
|
publicKey = c.getPublicKey(); |
|
|
|
ASN1Sequence algId = (ASN1Sequence) sequence.getObjectAt(2); |
|
algorithmIdentifier = new AlgorithmIdentifier( |
|
(ASN1ObjectIdentifier) algId.getObjectAt(0)); |
|
|
|
ASN1Sequence attrs = (ASN1Sequence) sequence.getObjectAt(3); |
|
target = (DERPrintableString) attrs.getObjectAt(0); |
|
length = (ASN1Integer) attrs.getObjectAt(1); |
|
|
|
this.signature = (DEROctetString) sequence.getObjectAt(4); |
|
} |
|
|
|
public ASN1Object getAuthenticatedAttributes() { |
|
ASN1EncodableVector attrs = new ASN1EncodableVector(); |
|
attrs.add(target); |
|
attrs.add(length); |
|
return new DERSequence(attrs); |
|
} |
|
|
|
public byte[] getEncodedAuthenticatedAttributes() throws IOException { |
|
return getAuthenticatedAttributes().getEncoded(); |
|
} |
|
|
|
public AlgorithmIdentifier getAlgorithmIdentifier() { |
|
return algorithmIdentifier; |
|
} |
|
|
|
public PublicKey getPublicKey() { |
|
return publicKey; |
|
} |
|
|
|
public byte[] getSignature() { |
|
return signature.getOctets(); |
|
} |
|
|
|
public void setSignature(byte[] sig, AlgorithmIdentifier algId) { |
|
algorithmIdentifier = algId; |
|
signature = new DEROctetString(sig); |
|
} |
|
|
|
public void setCertificate(X509Certificate cert) |
|
throws Exception, IOException, CertificateEncodingException { |
|
ASN1InputStream s = new ASN1InputStream(cert.getEncoded()); |
|
certificate = s.readObject(); |
|
publicKey = cert.getPublicKey(); |
|
} |
|
|
|
public byte[] generateSignableImage(byte[] image) throws IOException { |
|
byte[] attrs = getEncodedAuthenticatedAttributes(); |
|
byte[] signable = Arrays.copyOf(image, image.length + attrs.length); |
|
for (int i=0; i < attrs.length; i++) { |
|
signable[i+image.length] = attrs[i]; |
|
} |
|
return signable; |
|
} |
|
|
|
public byte[] sign(byte[] image, PrivateKey key) throws Exception { |
|
byte[] signable = generateSignableImage(image); |
|
return Utils.sign(key, signable); |
|
} |
|
|
|
public boolean verify(byte[] image) throws Exception { |
|
if (length.getValue().intValue() != image.length) { |
|
throw new IllegalArgumentException("Invalid image length"); |
|
} |
|
|
|
byte[] signable = generateSignableImage(image); |
|
return Utils.verify(publicKey, signable, signature.getOctets(), |
|
algorithmIdentifier); |
|
} |
|
|
|
public ASN1Primitive toASN1Primitive() { |
|
ASN1EncodableVector v = new ASN1EncodableVector(); |
|
v.add(formatVersion); |
|
v.add(certificate); |
|
v.add(algorithmIdentifier); |
|
v.add(getAuthenticatedAttributes()); |
|
v.add(signature); |
|
return new DERSequence(v); |
|
} |
|
|
|
public static int getSignableImageSize(byte[] data) throws Exception { |
|
if (!Arrays.equals(Arrays.copyOfRange(data, 0, 8), |
|
"ANDROID!".getBytes("US-ASCII"))) { |
|
throw new IllegalArgumentException("Invalid image header: missing magic"); |
|
} |
|
|
|
ByteBuffer image = ByteBuffer.wrap(data); |
|
image.order(ByteOrder.LITTLE_ENDIAN); |
|
|
|
image.getLong(); // magic |
|
int kernelSize = image.getInt(); |
|
image.getInt(); // kernel_addr |
|
int ramdskSize = image.getInt(); |
|
image.getInt(); // ramdisk_addr |
|
int secondSize = image.getInt(); |
|
image.getLong(); // second_addr + tags_addr |
|
int pageSize = image.getInt(); |
|
|
|
int length = pageSize // include the page aligned image header |
|
+ ((kernelSize + pageSize - 1) / pageSize) * pageSize |
|
+ ((ramdskSize + pageSize - 1) / pageSize) * pageSize |
|
+ ((secondSize + pageSize - 1) / pageSize) * pageSize; |
|
|
|
length = ((length + pageSize - 1) / pageSize) * pageSize; |
|
|
|
if (length <= 0) { |
|
throw new IllegalArgumentException("Invalid image header: invalid length"); |
|
} |
|
|
|
return length; |
|
} |
|
|
|
public static void doSignature( String target, |
|
String imagePath, |
|
String keyPath, |
|
String certPath, |
|
String outPath) throws Exception { |
|
|
|
byte[] image = Utils.read(imagePath); |
|
int signableSize = getSignableImageSize(image); |
|
|
|
if (signableSize < image.length) { |
|
System.err.println("NOTE: truncating file " + imagePath + |
|
" from " + image.length + " to " + signableSize + " bytes"); |
|
image = Arrays.copyOf(image, signableSize); |
|
} else if (signableSize > image.length) { |
|
throw new IllegalArgumentException("Invalid image: too short, expected " + |
|
signableSize + " bytes"); |
|
} |
|
|
|
BootSignature bootsig = new BootSignature(target, image.length); |
|
|
|
X509Certificate cert = Utils.loadPEMCertificate(certPath); |
|
bootsig.setCertificate(cert); |
|
|
|
PrivateKey key = Utils.loadDERPrivateKeyFromFile(keyPath); |
|
bootsig.setSignature(bootsig.sign(image, key), |
|
Utils.getSignatureAlgorithmIdentifier(key)); |
|
|
|
byte[] encoded_bootsig = bootsig.getEncoded(); |
|
byte[] image_with_metadata = Arrays.copyOf(image, image.length + encoded_bootsig.length); |
|
|
|
System.arraycopy(encoded_bootsig, 0, image_with_metadata, |
|
image.length, encoded_bootsig.length); |
|
|
|
Utils.write(image_with_metadata, outPath); |
|
} |
|
|
|
public static void verifySignature(String imagePath, String certPath) throws Exception { |
|
byte[] image = Utils.read(imagePath); |
|
int signableSize = getSignableImageSize(image); |
|
|
|
if (signableSize >= image.length) { |
|
throw new IllegalArgumentException("Invalid image: not signed"); |
|
} |
|
|
|
byte[] signature = Arrays.copyOfRange(image, signableSize, image.length); |
|
BootSignature bootsig = new BootSignature(signature); |
|
|
|
if (!certPath.isEmpty()) { |
|
System.err.println("NOTE: verifying using public key from " + certPath); |
|
bootsig.setCertificate(Utils.loadPEMCertificate(certPath)); |
|
} |
|
|
|
try { |
|
if (bootsig.verify(Arrays.copyOf(image, signableSize))) { |
|
System.err.println("Signature is VALID"); |
|
System.exit(0); |
|
} else { |
|
System.err.println("Signature is INVALID"); |
|
} |
|
} catch (Exception e) { |
|
e.printStackTrace(System.err); |
|
} |
|
System.exit(1); |
|
} |
|
|
|
/* Example usage for signing a boot image using dev keys: |
|
java -cp \ |
|
../../../out/host/common/obj/JAVA_LIBRARIES/BootSignature_intermediates/ \ |
|
classes/com.android.verity.BootSignature \ |
|
/boot \ |
|
../../../out/target/product/$PRODUCT/boot.img \ |
|
../../../build/target/product/security/verity.pk8 \ |
|
../../../build/target/product/security/verity.x509.pem \ |
|
/tmp/boot.img.signed |
|
*/ |
|
public static void main(String[] args) throws Exception { |
|
Security.addProvider(new BouncyCastleProvider()); |
|
|
|
if ("-verify".equals(args[0])) { |
|
String certPath = ""; |
|
|
|
if (args.length >= 4 && "-certificate".equals(args[2])) { |
|
/* args[3] is the path to a public key certificate */ |
|
certPath = args[3]; |
|
} |
|
|
|
/* args[1] is the path to a signed boot image */ |
|
verifySignature(args[1], certPath); |
|
} else { |
|
/* args[0] is the target name, typically /boot |
|
args[1] is the path to a boot image to sign |
|
args[2] is the path to a private key |
|
args[3] is the path to the matching public key certificate |
|
args[4] is the path where to output the signed boot image |
|
*/ |
|
doSignature(args[0], args[1], args[2], args[3], args[4]); |
|
} |
|
} |
|
}
|
|
|