/*
*
* Copyright (c) 2013 - 2017 Lijun Liao
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3
* as published by the Free Software Foundation with the addition of the
* following permission added to Section 15 as permitted in Section 7(a):
*
* FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
* THE AUTHOR LIJUN LIAO. LIJUN LIAO DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
* OF THIRD PARTY RIGHTS.
*
* 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License.
*
* You can be released from the requirements of the license by purchasing
* a commercial license. Buying such a license is mandatory as soon as you
* develop commercial activities involving the XiPKI software without
* disclosing the source code of your own applications.
*
* For more information, please contact Lijun Liao at this
* address: lijun.liao@gmail.com
*/
package org.xipki.commons.security.pkcs11.proxy;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.ASN1Sequence;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xipki.commons.common.ConfPairs;
import org.xipki.commons.common.util.IoUtil;
import org.xipki.commons.common.util.LogUtil;
import org.xipki.commons.common.util.ParamUtil;
import org.xipki.commons.common.util.StringUtil;
import org.xipki.commons.security.exception.BadAsn1ObjectException;
import org.xipki.commons.security.exception.P11TokenException;
import org.xipki.commons.security.pkcs11.AbstractP11Module;
import org.xipki.commons.security.pkcs11.P11Module;
import org.xipki.commons.security.pkcs11.P11ModuleConf;
import org.xipki.commons.security.pkcs11.P11Slot;
import org.xipki.commons.security.pkcs11.P11SlotIdentifier;
import org.xipki.commons.security.pkcs11.proxy.msg.Asn1P11SlotIdentifier;
import org.xipki.commons.security.pkcs11.proxy.msg.Asn1ServerCaps;
/**
* @author Lijun Liao
* @since 2.0.0
*/
public class ProxyP11Module extends AbstractP11Module {
public static final String PREFIX = "proxy:";
private static final Logger LOG = LoggerFactory.getLogger(ProxyP11Module.class);
private static final String REQUEST_MIMETYPE = "application/x-xipki-pkcs11";
private static final String RESPONSE_MIMETYPE = "application/x-xipki-pkcs11";
private final Random random = new Random();
private final short version = P11ProxyConstants.VERSION_V1_0;
private URL serverUrl;
private short moduleId;
private boolean readOnly;
private ProxyP11Module(final P11ModuleConf moduleConf) throws P11TokenException {
super(moduleConf);
final String modulePath = moduleConf.getNativeLibrary();
if (!StringUtil.startsWithIgnoreCase(modulePath, PREFIX)) {
throw new IllegalArgumentException("the module path does not starts with " + PREFIX
+ ": " + modulePath);
}
ConfPairs confPairs = new ConfPairs(modulePath.substring(PREFIX.length()));
String urlStr = confPairs.getValue("url");
try {
serverUrl = new URL(urlStr);
} catch (MalformedURLException ex) {
throw new IllegalArgumentException("invalid url: " + urlStr);
}
String moduleStr = confPairs.getValue("module");
if (moduleStr == null) {
throw new IllegalArgumentException("module not specified");
}
try {
moduleStr = moduleStr.trim();
if (moduleStr.startsWith("0x") || moduleStr.startsWith("0X")) {
moduleId = Short.parseShort(moduleStr.substring(2), 16);
} else {
moduleId = Short.parseShort(moduleStr.trim());
}
} catch (NumberFormatException ex) {
throw new IllegalArgumentException("invalid module: " + moduleStr);
}
refresh();
}
public static P11Module getInstance(final P11ModuleConf moduleConf) throws P11TokenException {
ParamUtil.requireNonNull("moduleConf", moduleConf);
return new ProxyP11Module(moduleConf);
}
@Override
public boolean isReadOnly() {
return readOnly || super.isReadOnly();
}
public void refresh() throws P11TokenException {
byte[] resp = send(P11ProxyConstants.ACTION_GET_SERVER_CAPS, null);
Asn1ServerCaps caps;
try {
caps = Asn1ServerCaps.getInstance(resp);
} catch (BadAsn1ObjectException ex) {
throw new P11TokenException("response is a valid Asn1ServerCaps", ex);
}
if (!caps.getVersions().contains(version)) {
throw new P11TokenException(
"Server does not support any version supported by the client");
}
this.readOnly = caps.isReadOnly();
resp = send(P11ProxyConstants.ACTION_GET_SLOT_IDS, null);
ASN1Sequence seq;
try {
seq = ASN1Sequence.getInstance(resp);
} catch (IllegalArgumentException ex) {
throw new P11TokenException("response is not ASN1Sequence", ex);
}
final int n = seq.size();
Set<P11Slot> slots = new HashSet<>();
for (int i = 0; i < n; i++) {
Asn1P11SlotIdentifier asn1SlotId;
try {
ASN1Encodable obj = seq.getObjectAt(i);
asn1SlotId = Asn1P11SlotIdentifier.getInstance(obj);
} catch (Exception ex) {
throw new P11TokenException(ex.getMessage(), ex);
}
P11SlotIdentifier slotId = asn1SlotId.getSlotId();
if (!conf.isSlotIncluded(slotId)) {
continue;
}
if (!conf.isSlotIncluded(slotId)) {
LOG.info("skipped slot {}", slotId);
continue;
}
P11Slot slot = new ProxyP11Slot(this, slotId, conf.isReadOnly(),
conf.getP11MechanismFilter());
slots.add(slot);
}
setSlots(slots);
}
@Override
public void close() {
for (P11SlotIdentifier slotId : getSlotIdentifiers()) {
try {
getSlot(slotId).close();
} catch (Throwable th) {
LogUtil.error(LOG, th, "could not close PKCS#11 slot " + slotId);
}
}
}
protected byte[] send(final byte[] request) throws IOException {
ParamUtil.requireNonNull("request", request);
HttpURLConnection httpUrlConnection = IoUtil.openHttpConn(serverUrl);
httpUrlConnection.setDoOutput(true);
httpUrlConnection.setUseCaches(false);
int size = request.length;
httpUrlConnection.setRequestMethod("POST");
httpUrlConnection.setRequestProperty("Content-Type", REQUEST_MIMETYPE);
httpUrlConnection.setRequestProperty("Content-Length", java.lang.Integer.toString(size));
OutputStream outputstream = httpUrlConnection.getOutputStream();
outputstream.write(request);
outputstream.flush();
if (httpUrlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
try {
try {
InputStream is = httpUrlConnection.getInputStream();
if (is != null) {
is.close();
}
} catch (IOException ex) {
InputStream errStream = httpUrlConnection.getErrorStream();
if (errStream != null) {
errStream.close();
}
}
} catch (Throwable th) {
// ignore it
}
throw new IOException("bad response: code=" + httpUrlConnection.getResponseCode()
+ ", message=" + httpUrlConnection.getResponseMessage());
}
InputStream inputstream = null;
try {
inputstream = httpUrlConnection.getInputStream();
} catch (IOException ex) {
InputStream errStream = httpUrlConnection.getErrorStream();
if (errStream != null) {
errStream.close();
}
throw ex;
}
try {
String responseContentType = httpUrlConnection.getContentType();
boolean isValidContentType = false;
if (responseContentType != null) {
if (responseContentType.equalsIgnoreCase(RESPONSE_MIMETYPE)) {
isValidContentType = true;
}
}
if (!isValidContentType) {
throw new IOException("bad response: mime type " + responseContentType
+ " is not supported!");
}
byte[] buf = new byte[4096];
ByteArrayOutputStream bytearrayoutputstream = new ByteArrayOutputStream();
do {
int readedByte = inputstream.read(buf);
if (readedByte == -1) {
break;
}
bytearrayoutputstream.write(buf, 0, readedByte);
}
while (true);
return bytearrayoutputstream.toByteArray();
} finally {
inputstream.close();
}
} // method send
/**
* The request is constructed as follows:
* <pre>
* 0 - - - 1 - - - 2 - - - 3 - - - 4 - - - 5 - - - 6 - - - 7 - - - 8
* | Version | Transaction ID | Body ... |
* | ... Length | Action | Module ID | Content... |
* | .Content | <-- 10 + Length (offset).
*
* </pre>
* @param action action
* @param content content
* @return result.
* @throws P11TokenException If error occurred.
*/
public byte[] send(final short action, final ASN1Object content)
throws P11TokenException {
byte[] encodedContent;
if (content == null) {
encodedContent = null;
} else {
try {
encodedContent = content.getEncoded();
} catch (IOException ex) {
throw new P11TokenException("could encode the content", ex);
}
}
int bodyLen = 4;
if (encodedContent != null) {
bodyLen += encodedContent.length;
}
byte[] request = new byte[10 + bodyLen];
// version
IoUtil.writeShort(version, request, 0);
// transaction id
byte[] transactionId = randomTransactionId();
System.arraycopy(transactionId, 0, request, 2, 4);
// length
IoUtil.writeInt(bodyLen, request, 6);
// action
IoUtil.writeShort(action, request, 10);
// module ID
IoUtil.writeShort(moduleId, request, 12);
//content
if (encodedContent != null) {
System.arraycopy(encodedContent, 0, request, 14, encodedContent.length);
}
byte[] response;
try {
response = send(request);
} catch (IOException ex) {
final String msg = "could not send the request";
LOG.error(msg + " {}", request);
throw new P11TokenException(msg + ": " + ex.getMessage(), ex);
}
int respLen = response.length;
if (respLen < 12) {
throw new P11TokenException("response too short");
}
// Length
int respBodyLen = IoUtil.parseInt(response, 6);
if (respBodyLen + 10 != respLen) {
throw new P11TokenException("message lengt unmatch");
}
// RC
short rc = IoUtil.parseShort(response, 10);
if (rc != 0) {
throw new P11TokenException(
"server returned RC " + P11ProxyConstants.getReturnCodeName(rc));
}
// Version
short respVersion = IoUtil.parseShort(response, 0);
if (version != respVersion) {
throw new P11TokenException("version of response and request unmatch");
}
// TransactionID
if (!equals(transactionId, response, 2)) {
throw new P11TokenException("version of response and request unmatch");
}
if (respLen < 14) {
throw new P11TokenException("too short successful response");
}
short respAction = IoUtil.parseShort(response, 12);
if (action != respAction) {
throw new P11TokenException("action of response and request unmatch");
}
int respContentLen = respLen - 14;
if (respContentLen == 0) {
return null;
}
byte[] respContent = new byte[respContentLen];
System.arraycopy(response, 14, respContent, 0, respContentLen);
return respContent;
} // method send
private byte[] randomTransactionId() {
byte[] tid = new byte[4];
random.nextBytes(tid);
return tid;
}
private static boolean equals(byte[] bytes, byte[] bytesB, int offsetB) {
if (bytesB.length - offsetB < bytes.length) {
return false;
}
for (int i = 0; i < bytes.length; i++) {
if (bytes[i] != bytesB[offsetB + i]) {
return false;
}
}
return true;
}
}