package net.i2p.data; /* * free (adj.): unencumbered; not under the control of others * Written by jrandom in 2003 and released into the public domain * with no warranty of any kind, either expressed or implied. * It probably won't make your computer catch on fire, or eat * your children, but it might. Use at your own risk. * */ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; /** * Defines a certificate that can be attached to various I2P structures, such * as RouterIdentity and Destination, allowing routers and clients to help * manage denial of service attacks and the network utilization. Certificates * can even be defined to include identifiable information signed by some * certificate authority, though that use probably isn't appropriate for an * anonymous network ;) * * Todo: Properly support multiple certificates * * @author jrandom */ public class Certificate extends DataStructureImpl { public final static Certificate NULL_CERT = new NullCert(); protected int _type; protected byte[] _payload; /** Specifies a null certificate type with no payload */ public final static int CERTIFICATE_TYPE_NULL = 0; /** specifies a Hashcash style certificate */ public final static int CERTIFICATE_TYPE_HASHCASH = 1; /** we should not be used for anything (don't use us in the netDb, in tunnels, or tell others about us) */ public final static int CERTIFICATE_TYPE_HIDDEN = 2; /** Signed with 40-byte Signature and (optional) 32-byte hash */ public final static int CERTIFICATE_TYPE_SIGNED = 3; public final static int CERTIFICATE_LENGTH_SIGNED_WITH_HASH = Signature.SIGNATURE_BYTES + Hash.HASH_LENGTH; /** Contains multiple certs */ public final static int CERTIFICATE_TYPE_MULTIPLE = 4; /** @since 0.9.12 */ public final static int CERTIFICATE_TYPE_KEY = 5; /** * If null, P256 key, or Ed25519 key cert, return immutable static instance, else create new * @throws DataFormatException if not enough bytes * @since 0.8.3 */ public static Certificate create(byte[] data, int off) throws DataFormatException { int type; byte[] payload; int length; try { type = data[off] & 0xff; length = (int) DataHelper.fromLong(data, off + 1, 2); if (type == 0 && length == 0) return NULL_CERT; // from here down roughly the same as readBytes() below if (length == 0) return new Certificate(type, null); payload = new byte[length]; System.arraycopy(data, off + 3, payload, 0, length); } catch (ArrayIndexOutOfBoundsException aioobe) { throw new DataFormatException("not enough bytes", aioobe); } if (type == CERTIFICATE_TYPE_KEY) { if (length == 4) { if (Arrays.equals(payload, KeyCertificate.Ed25519_PAYLOAD)) return KeyCertificate.ELG_Ed25519_CERT; if (Arrays.equals(payload, KeyCertificate.ECDSA256_PAYLOAD)) return KeyCertificate.ELG_ECDSA256_CERT; } try { return new KeyCertificate(payload); } catch (DataFormatException dfe) { throw new IllegalArgumentException(dfe); } } return new Certificate(type, payload); } /** * If null, P256 key, or Ed25519 key cert, return immutable static instance, else create new * @since 0.8.3 */ public static Certificate create(InputStream in) throws DataFormatException, IOException { int type = (int) DataHelper.readLong(in, 1); int length = (int) DataHelper.readLong(in, 2); if (type == 0 && length == 0) return NULL_CERT; // from here down roughly the same as readBytes() below if (length == 0) return new Certificate(type, null); byte[] payload = new byte[length]; int read = DataHelper.read(in, payload); if (read != length) throw new DataFormatException("Not enough bytes for the payload (read: " + read + " length: " + length + ')'); if (type == CERTIFICATE_TYPE_KEY) { if (length == 4) { if (Arrays.equals(payload, KeyCertificate.Ed25519_PAYLOAD)) return KeyCertificate.ELG_Ed25519_CERT; if (Arrays.equals(payload, KeyCertificate.ECDSA256_PAYLOAD)) return KeyCertificate.ELG_ECDSA256_CERT; } return new KeyCertificate(payload); } return new Certificate(type, payload); } public Certificate() { } /** * @throws IllegalArgumentException if type < 0 */ public Certificate(int type, byte[] payload) { if (type < 0) throw new IllegalArgumentException(); _type = type; _payload = payload; } /** */ public int getCertificateType() { return _type; } /** * @throws IllegalArgumentException if type < 0 * @throws IllegalStateException if already set */ public void setCertificateType(int type) { if (type < 0) throw new IllegalArgumentException(); if (_type != 0 && _type != type) throw new IllegalStateException("already set"); _type = type; } public byte[] getPayload() { return _payload; } /** * @throws IllegalStateException if already set */ public void setPayload(byte[] payload) { if (_payload != null) throw new IllegalStateException("already set"); _payload = payload; } /** * @throws IllegalStateException if already set */ public void readBytes(InputStream in) throws DataFormatException, IOException { if (_type != 0 || _payload != null) throw new IllegalStateException("already set"); _type = (int) DataHelper.readLong(in, 1); int length = (int) DataHelper.readLong(in, 2); if (length > 0) { _payload = new byte[length]; int read = read(in, _payload); if (read != length) throw new DataFormatException("Not enough bytes for the payload (read: " + read + " length: " + length + ")"); } } public void writeBytes(OutputStream out) throws DataFormatException, IOException { if (_type < 0) throw new DataFormatException("Invalid certificate type: " + _type); //if ((_type != 0) && (_payload == null)) throw new DataFormatException("Payload is required for non null type"); out.write((byte) _type); if (_payload != null) { DataHelper.writeLong(out, 2, _payload.length); out.write(_payload); } else { DataHelper.writeLong(out, 2, 0L); } } /** * @return the written length (NOT the new offset) */ public int writeBytes(byte target[], int offset) { int cur = offset; DataHelper.toLong(target, cur, 1, _type); cur++; if (_payload != null) { DataHelper.toLong(target, cur, 2, _payload.length); cur += 2; System.arraycopy(_payload, 0, target, cur, _payload.length); cur += _payload.length; } else { DataHelper.toLong(target, cur, 2, 0); cur += 2; } return cur - offset; } /** * @throws IllegalStateException if already set */ public int readBytes(byte source[], int offset) throws DataFormatException { if (_type != 0 || _payload != null) throw new IllegalStateException("already set"); if (source == null) throw new DataFormatException("Cert is null"); if (source.length < offset + 3) throw new DataFormatException("Cert is too small [" + source.length + " off=" + offset + "]"); int cur = offset; _type = source[cur] & 0xff; cur++; int length = (int)DataHelper.fromLong(source, cur, 2); cur += 2; if (length > 0) { if (length + cur > source.length) throw new DataFormatException("Payload on the certificate is insufficient (len=" + source.length + " off=" + offset + " cur=" + cur + " payloadLen=" + length); _payload = new byte[length]; System.arraycopy(source, cur, _payload, 0, length); cur += length; } return cur - offset; } public int size() { return 1 + 2 + (_payload != null ? _payload.length : 0); } /** * Up-convert this to a KeyCertificate * * @throws DataFormatException if cert type != CERTIFICATE_TYPE_KEY * @since 0.9.12 */ public KeyCertificate toKeyCertificate() throws DataFormatException { if (_type != CERTIFICATE_TYPE_KEY) throw new DataFormatException("type"); return new KeyCertificate(this); } @Override public boolean equals(Object object) { if (object == this) return true; if ((object == null) || !(object instanceof Certificate)) return false; Certificate cert = (Certificate) object; return _type == cert.getCertificateType() && Arrays.equals(_payload, cert.getPayload()); } @Override public int hashCode() { return _type + DataHelper.hashCode(_payload); } @Override public String toString() { StringBuilder buf = new StringBuilder(64); buf.append("[Certificate: type: "); if (getCertificateType() == CERTIFICATE_TYPE_NULL) buf.append("Null"); else if (getCertificateType() == CERTIFICATE_TYPE_KEY) buf.append("Key"); else if (getCertificateType() == CERTIFICATE_TYPE_HASHCASH) buf.append("HashCash"); else if (getCertificateType() == CERTIFICATE_TYPE_HIDDEN) buf.append("Hidden"); else if (getCertificateType() == CERTIFICATE_TYPE_SIGNED) buf.append("Signed"); else buf.append("Unknown type (").append(getCertificateType()).append(')'); if (_payload == null) { buf.append(" payload: null"); } else { buf.append(" payload size: ").append(_payload.length); if (getCertificateType() == CERTIFICATE_TYPE_HASHCASH) { buf.append(" Stamp: ").append(DataHelper.getUTF8(_payload)); } else if (getCertificateType() == CERTIFICATE_TYPE_SIGNED && _payload.length == CERTIFICATE_LENGTH_SIGNED_WITH_HASH) { buf.append(" Signed by hash: ").append(Base64.encode(_payload, Signature.SIGNATURE_BYTES, Hash.HASH_LENGTH)); } else { int len = 32; if (len > _payload.length) len = _payload.length; buf.append(" first ").append(len).append(" bytes: "); buf.append(DataHelper.toString(_payload, len)); } } buf.append("]"); return buf.toString(); } /** * An immutable null certificate. * @since 0.8.3 */ private static final class NullCert extends Certificate { private static final int NULL_LENGTH = 1 + 2; private static final byte[] NULL_DATA = new byte[NULL_LENGTH]; public NullCert() { // zero already //_type = CERTIFICATE_TYPE_NULL; } /** @throws RuntimeException always */ @Override public void setCertificateType(int type) { throw new RuntimeException("Data already set"); } /** @throws RuntimeException always */ @Override public void setPayload(byte[] payload) { throw new RuntimeException("Data already set"); } /** @throws RuntimeException always */ @Override public void readBytes(InputStream in) throws DataFormatException, IOException { throw new RuntimeException("Data already set"); } /** Overridden for efficiency */ @Override public void writeBytes(OutputStream out) throws IOException { out.write(NULL_DATA); } /** Overridden for efficiency */ @Override public int writeBytes(byte target[], int offset) { System.arraycopy(NULL_DATA, 0, target, offset, NULL_LENGTH); return NULL_LENGTH; } /** @throws RuntimeException always */ @Override public int readBytes(byte source[], int offset) throws DataFormatException { throw new RuntimeException("Data already set"); } /** Overridden for efficiency */ @Override public int size() { return NULL_LENGTH; } /** Overridden for efficiency */ @Override public int hashCode() { // must be the same as type + payload above return 0; } } }