Skip to content

Commit

Permalink
Merge pull request #337 from solver-it-sro/native-pkcs
Browse files Browse the repository at this point in the history
Native pkcs
  • Loading branch information
celuchmarek authored Nov 22, 2023
2 parents 66020a0 + bafd69a commit 2b947bf
Show file tree
Hide file tree
Showing 15 changed files with 271 additions and 47 deletions.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ buildNumber.properties
.mvn/timing.properties
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
.mvn/wrapper/maven-wrapper.jar
.idea/
.idea/workspace.xml
.idea/usage.statistics.xml
.idea/shelf
whitelabel.iml

# IDE - It's up to developer to decide, we don't want to force configuration
Expand All @@ -28,3 +30,5 @@ secret
.autogram

/fakeTokenDriver

/cache
18 changes: 18 additions & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 19 additions & 3 deletions .run/Main.run.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
<option name="MAIN_CLASS_NAME" value="digital.slovensko.autogram.Main" />
<module name="autogram" />
<option name="PROGRAM_PARAMETERS" value="--url=autogram://listen?protocol=http&amp;host=localhost&amp;port=37200&amp;origin=*&amp;language=sk" />
<option name="VM_PARAMETERS" value="--add-exports javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED" />
<option name="VM_PARAMETERS" value="--add-exports jdk.crypto.cryptoki/sun.security.pkcs11.wrapper=ALL-UNNAMED --add-opens java.base/java.security=ALL-UNNAMED --add-opens jdk.crypto.cryptoki/sun.security.pkcs11=ALL-UNNAMED" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="digital.slovensko.autogram.ui.*" />
Expand All @@ -16,4 +15,21 @@
<option name="Make" enabled="true" />
</method>
</configuration>
</component>
<configuration default="false" name="CLI" type="Application" factoryName="Application" nameIsGenerated="true">
<option name="ALTERNATIVE_JRE_PATH" value="$PROJECT_DIR$/target/jdkCache/LIBERICA_jdk17.0.7+7_linux_amd64-full" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
<option name="MAIN_CLASS_NAME" value="digital.slovensko.autogram.Main" />
<module name="autogram" />
<option name="PROGRAM_PARAMETERS" value="--cli -s pom.xml -t .autogram/pom_signed.asice --parents -d fake" />
<option name="VM_PARAMETERS" value="--add-exports jdk.crypto.cryptoki/sun.security.pkcs11.wrapper=ALL-UNNAMED --add-opens java.base/java.security=ALL-UNNAMED --add-opens jdk.crypto.cryptoki/sun.security.pkcs11=ALL-UNNAMED" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="digital.slovensko.autogram.ui.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>
11 changes: 8 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,16 @@
<version>3.11.0</version>

<configuration>
<release>${java.version}</release>
<fork>true</fork>
<executable>${jlink.jdk.path}${file.separator}bin${file.separator}javac</executable>
<source>17</source>
<target>17</target>
<source>${java.version}</source>
<target>${java.version}</target>
<compilerArgs>
<arg>--add-exports</arg>
<arg>jdk.crypto.cryptoki/sun.security.pkcs11.wrapper=ALL-UNNAMED</arg>
<arg>--add-modules</arg>
<arg>jdk.crypto.cryptoki</arg>
</compilerArgs>
</configuration>
</plugin>

Expand Down
22 changes: 14 additions & 8 deletions src/main/java/digital/slovensko/autogram/core/Autogram.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package digital.slovensko.autogram.core;

import digital.slovensko.autogram.core.errors.AutogramException;
import digital.slovensko.autogram.core.errors.BatchConflictException;
import digital.slovensko.autogram.core.errors.BatchNotStartedException;
import digital.slovensko.autogram.core.errors.ResponseNetworkErrorException;
import digital.slovensko.autogram.core.errors.UnrecognizedException;
import digital.slovensko.autogram.core.errors.*;
import digital.slovensko.autogram.core.visualization.DocumentVisualizationBuilder;
import digital.slovensko.autogram.core.visualization.UnsupportedVisualization;
import digital.slovensko.autogram.drivers.TokenDriver;
Expand Down Expand Up @@ -103,12 +99,14 @@ public void sign(SigningJob job, SigningKey signingKey) {
try {
job.signWithKeyAndRespond(signingKey);
ui.onUIThreadDo(() -> ui.onSigningSuccess(job));
} catch (ResponseNetworkErrorException e) {
onSigningFailed(e, job);
} catch (AutogramException e) {
onSigningFailed(e);
} catch (DSSException e) {
onSigningFailed(AutogramException.createFromDSSException(e));
} catch (IllegalArgumentException e) {
onSigningFailed(AutogramException.createFromIllegalArgumentException(e));
} catch (ResponseNetworkErrorException e) {
onSigningFailed(e, job);
} catch (Exception e) {
onSigningFailed(new UnrecognizedException(e));
}
Expand Down Expand Up @@ -145,7 +143,15 @@ public void batchSign(SigningJob job, String batchId) {
batch.addJob(batchId);

ui.onWorkThreadDo(() -> {
ui.signBatch(job, batch.getSigningKey());
try {
ui.signBatch(job, batch.getSigningKey());
} catch (KeyPinDifferentFromTokenPinException e) {
ui.onUIThreadDo(() -> {
ui.cancelBatch(batch);
});

throw e;
}
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public void onDocumentSigned(SignedDocument signedDocument) {

public void onDocumentSignFailed(AutogramException error) {
batch.onJobFailure();
if (!error.batchCanContinue())
batch.end();

responder.onDocumentSignFailed(error);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public String getDescription() {
public static AutogramException createFromDSSException(DSSException e) {
for (Throwable cause = e; cause != null && cause.getCause() != cause; cause = cause.getCause()) {
if (cause.getMessage() != null) {
if (cause instanceof KeyPinDifferentFromTokenPinException)
return (KeyPinDifferentFromTokenPinException) cause;
if (cause instanceof java.security.ProviderException && cause.getMessage().contains("slotListIndex is 0 but token only has 0 slots")) {
return new InitializationFailedException();
} else if (cause.getMessage().equals("CKR_FUNCTION_CANCELED")) {
Expand Down Expand Up @@ -67,4 +69,8 @@ public static AutogramException createFromIllegalArgumentException(IllegalArgume

return new UnrecognizedException(e);
}

public boolean batchCanContinue() {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package digital.slovensko.autogram.core.errors;

public class KeyPinDifferentFromTokenPinException extends AutogramException {
public KeyPinDifferentFromTokenPinException(Throwable e) {
super("Nepodporovaný PIN certifikátu", "PIN podpisového certifikátu je iný než PIN úložiska certifikátov. Ďalšie podpisovanie môže viesť k zablokovaniu karty!", "Použitý certifikát má nastavený atribút CKA_ALWAYS_AUTHENTICATE a jeho PIN je iný než PIN úložiska certifikátov. Toto zatiaľ nepodporujeme. Kontaktujte nás, prosím, na [email protected]", e);
}

@Override
public boolean batchCanContinue() {
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package digital.slovensko.autogram.drivers;

import digital.slovensko.autogram.core.errors.AutogramException;
import digital.slovensko.autogram.core.errors.KeyPinDifferentFromTokenPinException;
import eu.europa.esig.dss.enumerations.SignatureAlgorithm;
import eu.europa.esig.dss.model.DSSException;
import eu.europa.esig.dss.model.SignatureValue;
import eu.europa.esig.dss.model.ToBeSigned;
import eu.europa.esig.dss.token.DSSPrivateKeyEntry;
import eu.europa.esig.dss.token.KSPrivateKeyEntry;
import eu.europa.esig.dss.token.Pkcs11SignatureToken;
import eu.europa.esig.dss.token.PrefilledPasswordCallback;
import sun.security.pkcs11.wrapper.CK_ATTRIBUTE;
import sun.security.pkcs11.wrapper.PKCS11;
import sun.security.pkcs11.wrapper.PKCS11Constants;
import sun.security.pkcs11.wrapper.PKCS11Exception;

import java.lang.reflect.Field;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Objects;

public class NativePkcs11SignatureToken extends Pkcs11SignatureToken {
private static final long CKU_CONTEXT_SPECIFIC = 2L;

private final PrefilledPasswordCallback prefilledPasswordCallback;
private final boolean shouldProvidePasswordForCkaAA;

public NativePkcs11SignatureToken(String pkcsPath, PrefilledPasswordCallback prefilledPasswordCallback, int slotIndex, boolean shouldProvidePasswordForCkaAA) {
super(pkcsPath, prefilledPasswordCallback, -1, slotIndex, null);

this.prefilledPasswordCallback = prefilledPasswordCallback;
this.shouldProvidePasswordForCkaAA = shouldProvidePasswordForCkaAA;
}

private byte[] sign(final byte[] bytes, final String javaSignatureAlgorithm, final AlgorithmParameterSpec param, final DSSPrivateKeyEntry keyEntry) throws GeneralSecurityException {
if (!(keyEntry instanceof KSPrivateKeyEntry)) {
throw new IllegalArgumentException("Only KSPrivateKeyEntry are supported");
}

final Signature signature = getSignatureInstance(javaSignatureAlgorithm);
if (param != null) {
signature.setParameter(param);
}
var pk = ((KSPrivateKeyEntry) keyEntry).getPrivateKey();

signature.initSign(pk);
runContextSpecificLoginIfNeeded(signature, pk);
signature.update(bytes);
return signature.sign();
}

private void runContextSpecificLoginIfNeeded(Signature signature, PrivateKey pk) throws GeneralSecurityException {
try {
// TODO cache & short-circuit
var p11 = getP11(signature);
var sessionId = getSessionId(signature);
var keyID = getKeyID(pk);
var attrs = new CK_ATTRIBUTE[]{new CK_ATTRIBUTE(PKCS11Constants.CKA_ALWAYS_AUTHENTICATE)};

p11.C_GetAttributeValue(sessionId, keyID, attrs);
if (shouldProvidePasswordForCkaAA && isAlwaysAuthenticate(attrs))
p11.C_Login(sessionId, CKU_CONTEXT_SPECIFIC, prefilledPasswordCallback.getPassword());

} catch (PKCS11Exception e) {
if (e.getMessage().equals("CKR_PIN_INCORRECT"))
throw new KeyPinDifferentFromTokenPinException(e);

throw new GeneralSecurityException(e);
}
}

private static boolean isAlwaysAuthenticate(CK_ATTRIBUTE[] attrs) {
var result = attrs[0].pValue;
if (result instanceof byte[]) {
return ((byte[]) result)[0] == 1;
} else {
return false; // CKA_ALWAYS_AUTHENTICATE not found
}
}

private static long getKeyID(PrivateKey pk) {
try {
var keyIDHolderField = pk.getClass().getSuperclass().getDeclaredField("keyIDHolder");
keyIDHolderField.setAccessible(true);
var keyIDHolder = keyIDHolderField.get(pk);

var keyIDField = keyIDHolder.getClass().getDeclaredField("keyID");
keyIDField.setAccessible(true);

return (long) keyIDField.get(keyIDHolder);
} catch (IllegalAccessException | NoSuchFieldException e) {
throw new RuntimeException(e);
}
}

private static long getSessionId(Signature signature) {
try {
Field sigSpiField = signature.getClass().getDeclaredField("sigSpi");
sigSpiField.setAccessible(true);
var sigSpi = sigSpiField.get(signature);

var sessionField = sigSpi.getClass().getDeclaredField("session");
sessionField.setAccessible(true);
var session = sessionField.get(sigSpi);

var idField = session.getClass().getDeclaredField("id");
idField.setAccessible(true);

return (long) idField.get(session);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

private static PKCS11 getP11(Signature signature) {
try {
Field sigSpiField = signature.getClass().getDeclaredField("sigSpi");
sigSpiField.setAccessible(true);
var sigSpi = sigSpiField.get(signature);

var tokenField = sigSpi.getClass().getDeclaredField("token");
tokenField.setAccessible(true);
var token = tokenField.get(sigSpi);

var p11Field = token.getClass().getDeclaredField("p11");
p11Field.setAccessible(true);
var p11 = p11Field.get(token);

return (PKCS11) p11;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}

// copy & paste just to call overridden private sign method
@Override
public SignatureValue sign(ToBeSigned toBeSigned, SignatureAlgorithm signatureAlgorithm, DSSPrivateKeyEntry keyEntry) throws DSSException {
assertEncryptionAlgorithmValid(signatureAlgorithm, keyEntry);

final String javaSignatureAlgorithm = signatureAlgorithm.getJCEId();
final byte[] bytes = toBeSigned.getBytes();
AlgorithmParameterSpec param = null;
if (signatureAlgorithm.getMaskGenerationFunction() != null) {
param = createPSSParam(signatureAlgorithm.getDigestAlgorithm());
}

try {
final byte[] signatureValue = sign(bytes, javaSignatureAlgorithm, param, keyEntry);
SignatureValue value = new SignatureValue();
value.setAlgorithm(signatureAlgorithm);
value.setValue(signatureValue);
return value;
} catch (AutogramException e) {
throw e;
} catch (Exception e) {
throw new DSSException(String.format("Unable to sign : %s", e.getMessage()), e);
}
}

// copy & paste
private void assertEncryptionAlgorithmValid(SignatureAlgorithm signatureAlgorithm, DSSPrivateKeyEntry keyEntry) {
Objects.requireNonNull(signatureAlgorithm, "SignatureAlgorithm shall be provided.");
Objects.requireNonNull(signatureAlgorithm.getEncryptionAlgorithm(), "EncryptionAlgorithm shall be provided within the SignatureAlgorithm.");
Objects.requireNonNull(keyEntry, "keyEntry shall be provided.");
if (!signatureAlgorithm.getEncryptionAlgorithm().isEquivalent(keyEntry.getEncryptionAlgorithm())) {
throw new IllegalArgumentException(String.format("The provided SignatureAlgorithm '%s' cannot be used to sign with " +
"the token's implied EncryptionAlgorithm '%s'", signatureAlgorithm.getName(), keyEntry.getEncryptionAlgorithm().getName()));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package digital.slovensko.autogram.drivers;

import eu.europa.esig.dss.token.AbstractKeyStoreTokenConnection;
import eu.europa.esig.dss.token.Pkcs11SignatureToken;
import eu.europa.esig.dss.token.PrefilledPasswordCallback;

import java.nio.file.Path;
Expand All @@ -13,7 +12,8 @@ public PKCS11TokenDriver(String name, Path path, boolean needsPassword, String s
}

@Override
public AbstractKeyStoreTokenConnection createTokenWithPassword(Integer slotId, char[] password) {
return new Pkcs11SignatureToken(getPath().toString(), new PrefilledPasswordCallback(new KeyStore.PasswordProtection(password)), -1, slotId, null);
public AbstractKeyStoreTokenConnection createTokenWithPassword(Integer slotIndex, char[] password) {
// TODO: shouldProvidePasswordForCkaAA happens to correlate with needsPassword() for now, because eID is different. Might be changed in the future.
return new NativePkcs11SignatureToken(getPath().toString(), new PrefilledPasswordCallback(new KeyStore.PasswordProtection(password)), slotIndex, needsPassword());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public static ErrorResponse buildFromException(Exception e) {
case "BatchNotStartedException" -> new ErrorResponse(400, "BATCH_NOT_STARTED", (AutogramException) e);
case "BatchInvalidIdException" -> new ErrorResponse(404, "BATCH_NOT_FOUND", (AutogramException) e);
case "BatchConflictException" -> new ErrorResponse(400, "BATCH_CONFLICT", (AutogramException) e);
case "KeyPinDifferentFromTokenPin" -> new ErrorResponse(400, "CARD_NOT_SUPPORTED", (AutogramException) e);
default -> new ErrorResponse(500, "INTERNAL_ERROR", "Unexpected exception signing document", e.getMessage());
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public void onBatchStartSuccess(Batch batch) {
autogram.batchSign(job, batch.getBatchId());
} catch (AutogramException e) {
autogram.onSigningFailed(e);

break;
}
}
Expand Down
Loading

0 comments on commit 2b947bf

Please sign in to comment.