Add sshauthentication-api v1 support

This commit is contained in:
Christian Hagau
2017-10-05 00:00:00 +00:00
parent 83ab483fc7
commit 2619cb1db3
57 changed files with 3954 additions and 79 deletions

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2017 Vincent Breitmoser <v.breitmoser@mugenguild.com>
* Copyright (C) 2017 Christian Hagau <ach@hagau.se>
*
* 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.ssh;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.auto.value.AutoValue;
import org.bouncycastle.bcpg.HashAlgorithmTags;
import org.sufficientlysecure.keychain.Constants;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* AuthenticationData holds metadata pertaining to the signing of a
* AuthenticationParcel via a AuthenticationOperation
*/
@AutoValue
public abstract class AuthenticationData implements Parcelable {
public abstract long getAuthenticationMasterKeyId();
public abstract Long getAuthenticationSubKeyId();
@Nullable
public abstract List<Long> getAllowedAuthenticationKeyIds();
public abstract int getHashAlgorithm();
public static Builder builder() {
return new AutoValue_AuthenticationData.Builder()
.setAuthenticationMasterKeyId(Constants.key.none)
.setAuthenticationSubKeyId(Constants.key.none)
.setHashAlgorithm(HashAlgorithmTags.SHA512);
}
@AutoValue.Builder
public abstract static class Builder {
public abstract AuthenticationData build();
public abstract Builder setAuthenticationMasterKeyId(long authenticationMasterKeyId);
public abstract Builder setAuthenticationSubKeyId(Long authenticationSubKeyId);
public abstract Builder setHashAlgorithm(int hashAlgorithm);
abstract Builder setAllowedAuthenticationKeyIds(List<Long> allowedAuthenticationKeyIds);
public Builder setAllowedAuthenticationKeyIds(Collection<Long> allowedAuthenticationKeyIds) {
setAllowedAuthenticationKeyIds(Collections.unmodifiableList(new ArrayList<>(allowedAuthenticationKeyIds)));
return this;
}
}
}

View File

@@ -0,0 +1,246 @@
/*
* Copyright (C) 2017 Christian Hagau <ach@hagau.se>
*
* 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.ssh;
import android.content.Context;
import android.support.annotation.NonNull;
import org.bouncycastle.openpgp.PGPAuthenticationSignatureGenerator;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.operator.jcajce.NfcSyncPGPContentSignerBuilder;
import org.sufficientlysecure.keychain.operations.BaseOperation;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
import org.sufficientlysecure.keychain.pgp.PassphraseCacheInterface;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.KeyRepository;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Passphrase;
import java.util.Collection;
/**
* This class supports a single, low-level, authentication operation.
* <p/>
* The operation of this class takes a AuthenticationParcel
* as input, and signs the included challenge as parametrized in the
* AuthenticationData object. It returns its status
* and a possible signature as a AuthenticationResult.
* <p/>
*
* @see AuthenticationParcel
*/
public class AuthenticationOperation extends BaseOperation<AuthenticationParcel> {
private static final String TAG = "AuthenticationOperation";
public AuthenticationOperation(Context context, KeyRepository keyRepository) {
super(context, keyRepository, null);
}
@NonNull
@Override
public AuthenticationResult execute(AuthenticationParcel input,
CryptoInputParcel cryptoInput) {
return executeInternal(input.getAuthenticationData(), cryptoInput, input);
}
@NonNull
public AuthenticationResult execute(AuthenticationData data,
CryptoInputParcel cryptoInput,
AuthenticationParcel authenticationParcel) {
return executeInternal(data, cryptoInput, authenticationParcel);
}
/**
* Signs challenge based on given parameters
*/
private AuthenticationResult executeInternal(AuthenticationData data,
CryptoInputParcel cryptoInput,
AuthenticationParcel authenticationParcel) {
int indent = 0;
OperationLog log = new OperationLog();
log.add(LogType.MSG_AUTH, indent);
indent += 1;
Log.d(TAG, data.toString());
long opTime;
long startTime = System.currentTimeMillis();
byte[] signature;
byte[] challenge = authenticationParcel.getChallenge();
int hashAlgorithm = data.getHashAlgorithm();
long authMasterKeyId = data.getAuthenticationMasterKeyId();
Long authSubKeyId = data.getAuthenticationSubKeyId();
if (authSubKeyId == null) {
try { // Get the key id of the authentication key belonging to the master key id
authSubKeyId = mKeyRepository.getCachedPublicKeyRing(authMasterKeyId).getSecretAuthenticationId();
} catch (PgpKeyNotFoundException e) {
log.add(LogType.MSG_AUTH_ERROR_KEY_AUTH, indent);
return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log);
}
}
// Get keyring with the authentication key
CanonicalizedSecretKeyRing authKeyRing;
try {
authKeyRing = mKeyRepository.getCanonicalizedSecretKeyRing(authMasterKeyId);
} catch (KeyRepository.NotFoundException e) {
log.add(LogType.MSG_AUTH_ERROR_KEY_AUTH, indent);
return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log);
}
CanonicalizedSecretKey authKey = authKeyRing.getSecretKey(authSubKeyId);
// Make sure the client is allowed to access this key
Collection<Long> allowedAuthenticationKeyIds = data.getAllowedAuthenticationKeyIds();
if (allowedAuthenticationKeyIds != null && !allowedAuthenticationKeyIds.contains(authMasterKeyId)) {
// this key is in our db, but NOT allowed!
log.add(LogType.MSG_AUTH_ERROR_KEY_NOT_ALLOWED, indent + 1);
return new AuthenticationResult(AuthenticationResult.RESULT_KEY_DISALLOWED, log);
}
// Make sure key is not expired or revoked
if (authKeyRing.isExpired() || authKeyRing.isRevoked()
|| authKey.isExpired() || authKey.isRevoked()) {
log.add(LogType.MSG_AUTH_ERROR_REVOKED_OR_EXPIRED, indent);
return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log);
}
// Make sure the selected key is allowed to authenticate
if (!authKey.canAuthenticate()) {
log.add(LogType.MSG_AUTH_ERROR_KEY_AUTH, indent);
return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log);
}
CanonicalizedSecretKey.SecretKeyType secretKeyType;
try {
secretKeyType = mKeyRepository
.getCachedPublicKeyRing(authMasterKeyId)
.getSecretKeyType(authSubKeyId);
} catch (KeyRepository.NotFoundException e) {
log.add(LogType.MSG_AUTH_ERROR_KEY_AUTH, indent);
return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log);
}
switch (secretKeyType) {
case DIVERT_TO_CARD:
case PASSPHRASE_EMPTY: {
boolean isUnlocked;
try {
isUnlocked = authKey.unlock(new Passphrase());
} catch (PgpGeneralException e) {
log.add(LogType.MSG_AUTH_ERROR_UNLOCK, indent);
return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log);
}
if (!isUnlocked) {
throw new AssertionError(
"PASSPHRASE_EMPTY/DIVERT_TO_CARD keyphrase not unlocked with empty passphrase."
+ " This is a programming error!");
}
break;
}
case PASSPHRASE: {
Passphrase localPassphrase = cryptoInput.getPassphrase();
if (localPassphrase == null) {
try {
localPassphrase = getCachedPassphrase(authMasterKeyId, authKey.getKeyId());
} catch (PassphraseCacheInterface.NoSecretKeyException ignored) {
}
}
if (localPassphrase == null) {
log.add(LogType.MSG_AUTH_PENDING_PASSPHRASE, indent + 1);
return new AuthenticationResult(log,
RequiredInputParcel.createRequiredAuthenticationPassphrase(
authMasterKeyId, authKey.getKeyId()),
cryptoInput);
}
boolean isUnlocked;
try {
isUnlocked = authKey.unlock(localPassphrase);
} catch (PgpGeneralException e) {
log.add(LogType.MSG_AUTH_ERROR_UNLOCK, indent);
return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log);
}
if (!isUnlocked) {
log.add(LogType.MSG_AUTH_ERROR_BAD_PASSPHRASE, indent);
return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log);
}
break;
}
case GNU_DUMMY: {
log.add(LogType.MSG_AUTH_ERROR_UNLOCK, indent);
return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log);
}
default: {
throw new AssertionError("Unhandled SecretKeyType! (should not happen)");
}
}
PGPAuthenticationSignatureGenerator signatureGenerator;
try {
signatureGenerator = authKey.getAuthenticationSignatureGenerator(
hashAlgorithm, cryptoInput.getCryptoData());
} catch (PgpGeneralException e) {
log.add(LogType.MSG_AUTH_ERROR_NFC, indent);
return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log);
}
signatureGenerator.update(challenge, 0, challenge.length);
try {
signature = signatureGenerator.generate().getSignature();
} catch (NfcSyncPGPContentSignerBuilder.NfcInteractionNeeded e) {
// this secret key diverts to a OpenPGP card, thus requires user interaction
log.add(LogType.MSG_AUTH_PENDING_NFC, indent);
return new AuthenticationResult(log, RequiredInputParcel.createSecurityTokenAuthenticationOperation(
authKey.getRing().getMasterKeyId(), authKey.getKeyId(),
e.hashToSign, e.hashAlgo), cryptoInput);
} catch (PGPException e) {
log.add(LogType.MSG_AUTH_ERROR_SIG, indent);
return new AuthenticationResult(AuthenticationResult.RESULT_ERROR, log);
}
opTime = System.currentTimeMillis() - startTime;
Log.d(TAG, "Authentication operation duration : " + String.format("%.2f", opTime / 1000.0) + "s");
log.add(LogType.MSG_AUTH_OK, indent);
AuthenticationResult result = new AuthenticationResult(AuthenticationResult.RESULT_OK, log);
result.setSignature(signature);
result.mOperationTime = opTime;
return result;
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
* Copyright (C) 2017 Christian Hagau <ach@hagau.se>
*
* 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.ssh;
import android.os.Parcelable;
import com.google.auto.value.AutoValue;
/**
* AuthenticationParcel holds the challenge to be signed for authentication
*/
@AutoValue
public abstract class AuthenticationParcel implements Parcelable {
public abstract AuthenticationData getAuthenticationData();
@SuppressWarnings("mutable")
public abstract byte[] getChallenge();
public static AuthenticationParcel createAuthenticationParcel(AuthenticationData authenticationData,
byte[] challenge) {
return new AutoValue_AuthenticationParcel(authenticationData, challenge);
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
* Copyright (C) 2017 Christian Hagau <ach@hagau.se>
*
* 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.ssh;
import android.os.Parcel;
import org.sufficientlysecure.keychain.operations.results.InputPendingResult;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
/**
* AuthenticationResult holds the result of a AuthenticationOperation
*/
public class AuthenticationResult extends InputPendingResult {
public static final int RESULT_KEY_DISALLOWED = RESULT_ERROR + 32;
byte[] mSignature;
public long mOperationTime;
public void setSignature(byte[] signature) {
mSignature = signature;
}
public byte[] getSignature() {
return mSignature;
}
public AuthenticationResult(int result, OperationLog log) {
super(result, log);
}
public AuthenticationResult(OperationLog log, RequiredInputParcel requiredInput,
CryptoInputParcel cryptoInputParcel) {
super(log, requiredInput, cryptoInputParcel);
}
public AuthenticationResult(Parcel source) {
super(source);
mSignature = source.readInt() != 0 ? source.createByteArray() : null;
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
if (mSignature != null) {
dest.writeInt(1);
dest.writeByteArray(mSignature);
} else {
dest.writeInt(0);
}
}
public static final Creator<AuthenticationResult> CREATOR = new Creator<AuthenticationResult>() {
public AuthenticationResult createFromParcel(final Parcel source) {
return new AuthenticationResult(source);
}
public AuthenticationResult[] newArray(final int size) {
return new AuthenticationResult[size];
}
};
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (C) 2017 Christian Hagau <ach@hagau.se>
*
* 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.ssh.key;
import java.math.BigInteger;
public class SshDSAPublicKey extends SshPublicKey {
public static final String KEY_ID = "ssh-dss";
private BigInteger mP;
private BigInteger mQ;
private BigInteger mG;
private BigInteger mY;
public SshDSAPublicKey(BigInteger p, BigInteger q, BigInteger g, BigInteger y) {
super(KEY_ID);
mP = p;
mQ = q;
mG = g;
mY = y;
}
@Override
protected void putData(SshEncodedData data) {
data.putMPInt(mP);
data.putMPInt(mQ);
data.putMPInt(mG);
data.putMPInt(mY);
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (C) 2017 Christian Hagau <ach@hagau.se>
*
* 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.ssh.key;
import java.math.BigInteger;
public class SshECDSAPublicKey extends SshPublicKey {
public static final String KEY_ID = "ecdsa-sha2-";
private BigInteger mQ;
private String mCurve;
public SshECDSAPublicKey(String curve, BigInteger q) {
super(KEY_ID + curve);
mCurve = curve;
mQ = q;
}
@Override
protected void putData(SshEncodedData data) {
data.putString(mCurve);
data.putString(mQ.toByteArray());
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2017 Christian Hagau <ach@hagau.se>
*
* 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.ssh.key;
public class SshEd25519PublicKey extends SshPublicKey {
public static final String KEY_ID = "ssh-ed25519";
private byte[] mAbyte;
public SshEd25519PublicKey(byte[] aByte) {
super(KEY_ID);
mAbyte = aByte;
}
@Override
protected void putData(SshEncodedData data) {
data.putString(mAbyte);
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright (C) 2017 Christian Hagau <ach@hagau.se>
*
* 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 org.sufficientlysecure.keychain.ssh.key;
import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
public class SshEncodedData {
private ByteArrayOutputStream mData;
public SshEncodedData() {
this(64);
}
public SshEncodedData(int initialLength) {
mData = new ByteArrayOutputStream(initialLength);
}
public void putString(String string) {
byte[] buffer = string.getBytes();
putString(buffer);
}
public void putString(byte[] buffer) {
putUInt32(buffer.length);
mData.write(buffer, 0, buffer.length);
}
public void putMPInt(BigInteger mpInt) {
byte buffer[] = mpInt.toByteArray();
if ((buffer.length == 1) && (buffer[0] == 0)) {
putUInt32(0);
} else {
putString(buffer);
}
}
public void putUInt32(int uInt) {
mData.write(uInt >> 24);
mData.write(uInt >> 16);
mData.write(uInt >> 8);
mData.write(uInt);
}
public void putByte(byte octet) {
mData.write(octet);
}
public void putBoolean(boolean flag) {
if (flag) {
mData.write(1);
} else {
mData.write(0);
}
}
public byte[] getBytes() {
return mData.toByteArray();
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (C) 2017 Christian Hagau <ach@hagau.se>
*
* 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.ssh.key;
import android.util.Base64;
public abstract class SshPublicKey {
protected SshEncodedData mData;
private String mKeyType;
public SshPublicKey(String keytype) {
mData = new SshEncodedData();
mKeyType = keytype;
mData.putString(mKeyType);
}
protected abstract void putData(SshEncodedData data);
public String getPublicKeyBlob() {
String publicKeyBlob = "";
publicKeyBlob += mKeyType + " ";
putData(mData);
String keyBlob = Base64.encodeToString(mData.getBytes(), Base64.NO_WRAP);
publicKeyBlob += keyBlob;
return publicKeyBlob;
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2017 Christian Hagau <ach@hagau.se>
*
* 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.ssh.key;
import java.math.BigInteger;
public class SshRSAPublicKey extends SshPublicKey {
public static final String KEY_ID = "ssh-rsa";
private BigInteger mExponent;
private BigInteger mModulus;
public SshRSAPublicKey(BigInteger exponent, BigInteger modulus) {
super(KEY_ID);
mExponent = exponent;
mModulus = modulus;
}
@Override
protected void putData(SshEncodedData data) {
data.putMPInt(mExponent);
data.putMPInt(mModulus);
}
}