/*
* Copyright (c) 2012 Mike Heath. All rights reserved.
*
* 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 cloudeventbus.pki;
import cloudeventbus.CloudEventBusException;
import cloudeventbus.Subject;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* @author Mike Heath <elcapo@gmail.com>
*/
// TODO Add an issuedDate field
public class Certificate {
public enum Type {
AUTHORITY,
CLIENT,
SERVER
}
static final int KEY_LENGTH = 294;
static final int MAX_STRING_LENGTH = 16384;
static final int SIGNATURE_LENGTH = 256;
private final Type type;
private final long serialNumber;
private final long issuer;
private final long expirationDate;
private final PublicKey publicKey;
private final List<Subject> subscribePermissions;
private final List<Subject> publishPermissions;
private final String comment;
private final byte[] signature;
private volatile byte[] hash;
public Certificate(Type type, long serialNumber, long issuer, long expirationDate, PublicKey publicKey, List<Subject> subscribePermissions, List<Subject> publishPermissions, String comment, byte[] signature) {
if (type == null) {
throw new IllegalArgumentException("type cannot be null");
}
this.type = type;
this.serialNumber = serialNumber;
this.issuer = issuer;
this.expirationDate = expirationDate;
if (publicKey == null) {
throw new IllegalArgumentException("public key cannot be null");
}
this.publicKey = publicKey;
this.subscribePermissions = Collections.unmodifiableList(new ArrayList<>(subscribePermissions));
this.publishPermissions = Collections.unmodifiableList(new ArrayList<>(publishPermissions));
this.comment = comment;
if (signature != null && signature.length != SIGNATURE_LENGTH) {
throw new InvalidCertificateException("Invalid signature, signature must be " + SIGNATURE_LENGTH + " bytes long.");
}
this.signature = signature;
}
public Certificate(InputStream in) throws IOException {
final DataInputStream data = new DataInputStream(in);
try {
this.type = Type.values()[data.read()];
serialNumber = data.readLong();
issuer = data.readLong();
expirationDate = data.readLong();
// Read and decode public key
final byte[] key = new byte[KEY_LENGTH];
data.readFully(key);
final KeyFactory keyFactory;
keyFactory = KeyFactory.getInstance("RSA");
final X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(key);
publicKey = keyFactory.generatePublic(publicKeySpec);
final String subscribe = readString(data);
subscribePermissions = commaSeperateStringToList(subscribe);
final String publish = readString(data);
publishPermissions = commaSeperateStringToList(publish);
final String commentString = readString(data);
comment = commentString.length() == 0 ? null : commentString;
final byte[] signature = new byte[SIGNATURE_LENGTH];
final int readBytes = data.read(signature);
if (readBytes <= 0) {
this.signature = null;
} else if (readBytes != SIGNATURE_LENGTH) {
throw new EOFException("Premature end of certificate");
} else {
this.signature = signature;
}
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new CloudEventBusException("Error decoding simple certificate", e);
}
}
public void store(OutputStream out) throws IOException {
store(out, true);
}
private void store(OutputStream out, boolean writeSignature) throws IOException {
final DataOutputStream data = new DataOutputStream(out);
data.write(type.ordinal());
data.writeLong(serialNumber);
data.writeLong(issuer);
data.writeLong(expirationDate);
data.write(publicKey.getEncoded());
data.write(listToCommaSeparatedString(subscribePermissions).getBytes());
data.write(0);
data.write(listToCommaSeparatedString(publishPermissions).getBytes());
data.write(0);
data.write((comment == null ? "" : comment).getBytes());
data.write(0);
if (writeSignature && signature != null) {
data.write(signature);
}
data.flush();
}
public byte[] hash() {
if (hash != null) {
return hash.clone();
}
try {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
store(out, false);
final MessageDigest digest = MessageDigest.getInstance("SHA-1");
hash = digest.digest(out.toByteArray());
return hash.clone();
} catch (IOException | NoSuchAlgorithmException e) {
throw new CloudEventBusException("Error calculating hash of certificate", e);
}
}
private String listToCommaSeparatedString(List<Subject> list) {
final StringBuilder buff = new StringBuilder();
String sep = "";
for (Subject s : list) {
buff.append(sep);
buff.append(s);
sep = ",";
}
return buff.toString();
}
private List<Subject> commaSeperateStringToList(String subscribe) {
final String[] parts = subscribe.split("\\s*,\\s*");
return Subject.list(parts);
}
private String readString(DataInputStream data) throws IOException {
data.mark(MAX_STRING_LENGTH);
int length = 0;
while (data.read() > 0) {
length++;
}
data.reset();
final byte[] string = new byte[length];
final int bytesRead = data.read(string);
if (bytesRead != length) {
throw new IOException("Error reading string from certificate");
}
data.read(); // Throw away null terminating byte
return new String(string);
}
public Type getType() {
return type;
}
public long getSerialNumber() {
return serialNumber;
}
public long getIssuer() {
return issuer;
}
public long getExpirationDate() {
return expirationDate;
}
public PublicKey getPublicKey() {
return publicKey;
}
public List<Subject> getSubscribePermissions() {
return subscribePermissions;
}
public List<Subject> getPublishPermissions() {
return publishPermissions;
}
public String getComment() {
return comment;
}
public byte[] getSignature() {
return signature.clone();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Certificate that = (Certificate) o;
if (expirationDate != that.expirationDate) return false;
if (issuer != that.issuer) return false;
if (serialNumber != that.serialNumber) return false;
if (comment != null ? !comment.equals(that.comment) : that.comment != null) return false;
if (!publicKey.equals(that.publicKey)) return false;
if (!publishPermissions.equals(that.publishPermissions)) return false;
if (!Arrays.equals(signature, that.signature)) return false;
if (!subscribePermissions.equals(that.subscribePermissions)) return false;
if (type != that.type) return false;
return true;
}
@Override
public int hashCode() {
int result = (int) (serialNumber ^ (serialNumber >>> 32));
result = 31 * result + (int) (issuer ^ (issuer >>> 32));
result = 31 * result + (int) (expirationDate ^ (expirationDate >>> 32));
result = 31 * result + publicKey.hashCode();
result = 31 * result + subscribePermissions.hashCode();
result = 31 * result + publishPermissions.hashCode();
result = 31 * result + (comment != null ? comment.hashCode() : 0);
result = 31 * result + (signature != null ? Arrays.hashCode(signature) : 0);
return result;
}
/**
* Validates that {@code certificate} was signed using this certificate's private key.
*
* @param certificate the certificate to validate.
* @throws CertificateIssuerMismatchException if {@code certificate.getIssuer()} doesn't match this certificates
* serial number.
* @throws CertificateSecurityException if a {@link GeneralSecurityException} occurs attempting to validate
* {@code certificate}'s signature.
* @throws InvalidCertificateSignatureException if {@code certificate}'s signature is invalid.
*/
public void validateSignature(Certificate certificate) {
if (certificate.getIssuer() != serialNumber) {
throw new CertificateIssuerMismatchException("The issuer of certificate " + certificate.getSerialNumber() + " does not match this certificate.");
}
try {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
final byte[] decryptedSignature = cipher.doFinal(certificate.getSignature());
if (!Arrays.equals(certificate.hash(), decryptedSignature)) {
throw new InvalidCertificateSignatureException("The signature on certificate " + certificate.getSerialNumber() + " is not valid.");
}
if (type != Type.AUTHORITY && type != certificate.type) {
throw new InvalidCertificateException("Certificates of type " + type + " cannot sign certificates of type " + certificate.getType());
}
} catch (GeneralSecurityException e) {
throw new CertificateSecurityException(e);
}
}
public void validatePublishPermission(Subject subject) {
for (Subject publishSubject : publishPermissions) {
if (!publishSubject.isSub(subject)) {
throw new CertificatePermissionError ("Permission " + subject + " is not granted by certificate.");
}
}
}
public void validateSubscribePermission(Subject subject) {
for (Subject subscribeSubject : subscribePermissions) {
if (!subscribeSubject.isSub(subject)) {
throw new CertificatePermissionError ("Permission " + subject + " is not granted by certificate.");
}
}
}
}