open-keychain/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenPsoSignTokenOp...

210 lines
8.9 KiB
Java

/*
* Copyright (C) 2018 Schürmann & Breitmoser GbR
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.securitytoken.operations;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1OutputStream;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.bcpg.HashAlgorithmTags;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.securitytoken.CardException;
import org.sufficientlysecure.keychain.securitytoken.CommandApdu;
import org.sufficientlysecure.keychain.securitytoken.EcKeyFormat;
import org.sufficientlysecure.keychain.securitytoken.KeyFormat;
import org.sufficientlysecure.keychain.securitytoken.OpenPgpCapabilities;
import org.sufficientlysecure.keychain.securitytoken.RsaKeyFormat;
import org.sufficientlysecure.keychain.securitytoken.ResponseApdu;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection;
import timber.log.Timber;
public class SecurityTokenPsoSignTokenOp {
private final SecurityTokenConnection connection;
public static SecurityTokenPsoSignTokenOp create(SecurityTokenConnection connection) {
return new SecurityTokenPsoSignTokenOp(connection);
}
private SecurityTokenPsoSignTokenOp(SecurityTokenConnection connection) {
this.connection = connection;
}
private byte[] prepareDsi(byte[] hash, int hashAlgo) throws IOException {
byte[] dsi;
Timber.i("Hash: " + hashAlgo);
switch (hashAlgo) {
case HashAlgorithmTags.SHA1:
if (hash.length != 20) {
throw new IOException("Bad hash length (" + hash.length + ", expected 10!");
}
dsi = Arrays.concatenate(Hex.decode(
"3021" // Tag/Length of Sequence, the 0x21 includes all following 33 bytes
+ "3009" // Tag/Length of Sequence, the 0x09 are the following header bytes
+ "0605" + "2B0E03021A" // OID of SHA1
+ "0500" // TLV coding of ZERO
+ "0414"), hash); // 0x14 are 20 hash bytes
break;
case HashAlgorithmTags.RIPEMD160:
if (hash.length != 20) {
throw new IOException("Bad hash length (" + hash.length + ", expected 20!");
}
dsi = Arrays.concatenate(Hex.decode("3021300906052B2403020105000414"), hash);
break;
case HashAlgorithmTags.SHA224:
if (hash.length != 28) {
throw new IOException("Bad hash length (" + hash.length + ", expected 28!");
}
dsi = Arrays.concatenate(Hex.decode("302D300D06096086480165030402040500041C"), hash);
break;
case HashAlgorithmTags.SHA256:
if (hash.length != 32) {
throw new IOException("Bad hash length (" + hash.length + ", expected 32!");
}
dsi = Arrays.concatenate(Hex.decode("3031300D060960864801650304020105000420"), hash);
break;
case HashAlgorithmTags.SHA384:
if (hash.length != 48) {
throw new IOException("Bad hash length (" + hash.length + ", expected 48!");
}
dsi = Arrays.concatenate(Hex.decode("3041300D060960864801650304020205000430"), hash);
break;
case HashAlgorithmTags.SHA512:
if (hash.length != 64) {
throw new IOException("Bad hash length (" + hash.length + ", expected 64!");
}
dsi = Arrays.concatenate(Hex.decode("3051300D060960864801650304020305000440"), hash);
break;
default:
throw new IOException("Not supported hash algo!");
}
return dsi;
}
private byte[] prepareData(byte[] hash, int hashAlgo, KeyFormat keyFormat) throws IOException {
if (keyFormat instanceof RsaKeyFormat) {
return prepareDsi(hash, hashAlgo);
} else if (keyFormat instanceof EcKeyFormat) {
return hash;
} else {
throw new IOException("Not supported key type!");
}
}
private byte[] encodeSignature(byte[] signature, KeyFormat keyFormat) throws IOException {
// Make sure the signature we received is actually the expected number of bytes long!
if (keyFormat instanceof RsaKeyFormat) {
// no encoding necessary
int modulusLength = ((RsaKeyFormat) keyFormat).modulusLength();
if (signature.length != (modulusLength / 8)) {
throw new IOException("Bad signature length! Expected " + (modulusLength / 8) +
" bytes, got " + signature.length);
}
return signature;
} else if (keyFormat instanceof EcKeyFormat) {
EcKeyFormat ecKeyFormat = (EcKeyFormat) keyFormat;
if (ecKeyFormat.isEdDsa()) {
return signature;
}
// "plain" encoding, see https://github.com/open-keychain/open-keychain/issues/2108
if (signature.length % 2 != 0) {
throw new IOException("Bad signature length!");
}
byte[] br = new byte[signature.length / 2];
byte[] bs = new byte[signature.length / 2];
for (int i = 0; i < br.length; ++i) {
br[i] = signature[i];
bs[i] = signature[br.length + i];
}
if (br[0] == 0x00 && (br[1] & 0x80) == 0) {
br = Arrays.copyOfRange(br, 1, br.length);
}
if (bs[0] == 0x00 && (bs[1] & 0x80) == 0) {
bs = Arrays.copyOfRange(bs, 1, bs.length);
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ASN1OutputStream out = ASN1OutputStream.create(baos);
out.writeObject(new DERSequence(new ASN1Encodable[]{new ASN1Integer(br), new ASN1Integer(bs)}));
out.flush();
return baos.toByteArray();
} else {
throw new IOException("Not supported key format!");
}
}
/**
* Call COMPUTE DIGITAL SIGNATURE command and returns the MPI value
*
* @param hash the hash for signing
* @return a big integer representing the MPI for the given hash
*/
public byte[] calculateSignature(byte[] hash, int hashAlgo) throws IOException {
connection.verifyPinForSignature();
OpenPgpCapabilities openPgpCapabilities = connection.getOpenPgpCapabilities();
KeyFormat signKeyFormat = openPgpCapabilities.getSignKeyFormat();
byte[] data = prepareData(hash, hashAlgo, signKeyFormat);
// Command APDU for PERFORM SECURITY OPERATION: COMPUTE DIGITAL SIGNATURE (page 37)
CommandApdu command = connection.getCommandFactory().createComputeDigitalSignatureCommand(data);
ResponseApdu response = connection.communicate(command);
connection.invalidateSingleUsePw1();
if (!response.isSuccess()) {
throw new CardException("Failed to sign", response.getSw());
}
return encodeSignature(response.getData(), signKeyFormat);
}
/**
* Call INTERNAL AUTHENTICATE command and returns the MPI value
*
* @param hash the hash for signing
* @return a big integer representing the MPI for the given hash
*/
public byte[] calculateAuthenticationSignature(byte[] hash, int hashAlgo) throws IOException {
connection.verifyPinForOther();
OpenPgpCapabilities openPgpCapabilities = connection.getOpenPgpCapabilities();
KeyFormat authKeyFormat = openPgpCapabilities.getAuthKeyFormat();
byte[] data = prepareData(hash, hashAlgo, authKeyFormat);
// Command APDU for INTERNAL AUTHENTICATE (page 55)
CommandApdu command = connection.getCommandFactory().createInternalAuthCommand(data);
ResponseApdu response = connection.communicate(command);
if (!response.isSuccess()) {
throw new CardException("Failed to sign", response.getSw());
}
return encodeSignature(response.getData(), authKeyFormat);
}
}