/*
* JBoss, Home of Professional Open Source.
* Copyright 2015 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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.wildfly.security.pem;
import static org.wildfly.security._private.ElytronMessages.log;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.X509EncodedKeySpec;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.wildfly.common.Assert;
import org.wildfly.security.asn1.ASN1;
import org.wildfly.security.asn1.DERDecoder;
import org.wildfly.security.util.ByteIterator;
import org.wildfly.security.util.ByteStringBuilder;
import org.wildfly.security.util.CodePointIterator;
/**
* A class containing utilities which can handle the PEM format. See <a href="https://tools.ietf.org/html/rfc7468">RFC 7468</a>
* for more information.
*
* @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
*/
public final class Pem {
private static final Pattern VALID_LABEL = Pattern.compile("[^ -~&&[^-]]");
private static final String PUBLIC_KEY_FORMAT = "PUBLIC KEY";
private static final String CERTIFICATE_FORMAT = "CERTIFICATE";
/**
* Parse arbitrary PEM content. The given function is used to parse the content of the PEM representation and produce
* some result. The PEM type string is passed to the function. If the function throws an exception, that exception
* is propagated to the caller of this method. If the PEM content is malformed, an exception is thrown. If the
* trailing PEM content is found to be invalid after the function returns, the function result is discarded and an
* exception is thrown.
*
* @param pemContent the content to parse (must not be {@code null})
* @param contentFunction a function to consume the PEM content and produce a result (must not be {@code null})
* @param <R> the value return type
* @return the return value of the function
* @throws IllegalArgumentException if there is a problem with processing the content of the PEM data
*/
public static <R> R parsePemContent(CodePointIterator pemContent, BiFunction<String, ByteIterator, R> contentFunction) throws IllegalArgumentException {
Assert.checkNotNullParam("pemContent", pemContent);
Assert.checkNotNullParam("contentFunction", contentFunction);
pemContent = pemContent.skipCrLf();
if (! pemContent.limitedTo(11).contentEquals("-----BEGIN ")) {
throw log.malformedPemContent(pemContent.offset());
}
String type = pemContent.delimitedBy('-').drainToString().trim();
final Matcher matcher = VALID_LABEL.matcher(type);
if (matcher.find()) {
// BEGIN string is 11 chars long
throw log.malformedPemContent(matcher.start() + 11);
}
if (! pemContent.limitedTo(5).contentEquals("-----")) {
throw log.malformedPemContent(pemContent.offset());
}
final ByteIterator byteIterator = pemContent.delimitedBy('-').base64Decode();
final R result = contentFunction.apply(type, byteIterator);
if (! pemContent.limitedTo(9).contentEquals("-----END ")) {
throw log.malformedPemContent(pemContent.offset());
}
if (! pemContent.limitedTo(type.length()).contentEquals(type)) {
throw log.malformedPemContent(pemContent.offset());
}
if (! pemContent.limitedTo(5).contentEquals("-----")) {
throw log.malformedPemContent(pemContent.offset());
}
return result;
}
/**
* Iterate over the contents of a PEM file, returning each entry in sequence.
*
* @param pemContent the code point iterator over the content (must not be {@code null})
* @return the iterator (not {@code null})
*/
public static Iterator<PemEntry<?>> parsePemContent(CodePointIterator pemContent) {
return new Iterator<PemEntry<?>>() {
private PemEntry<?> next;
public boolean hasNext() {
if (next == null) {
if (! pemContent.hasNext()) {
return false;
}
next = parsePemContent(pemContent, (label, byteIterator) -> {
switch (label) {
case CERTIFICATE_FORMAT: {
final X509Certificate x509Certificate = parsePemX509CertificateContent(label, byteIterator);
return new PemEntry<>(x509Certificate);
}
case PUBLIC_KEY_FORMAT: {
final PublicKey publicKey = parsePemPublicKey(label, byteIterator);
return new PemEntry<>(publicKey);
}
default: {
throw log.malformedPemContent(pemContent.offset());
}
}
});
}
return true;
}
public PemEntry<?> next() {
if (! hasNext()) {
throw new NoSuchElementException();
}
try {
return next;
} finally {
next = null;
}
}
};
}
/**
* Generate PEM content to the given byte string builder. The appropriate header and footer surrounds the base-64
* encoded value.
*
* @param target the target byte string builder (must not be {@code null})
* @param type the content type (must not be {@code null})
* @param content the content iterator (must not be {@code null})
* @throws IllegalArgumentException if there is a problem with the data or the type
*/
public static void generatePemContent(ByteStringBuilder target, String type, ByteIterator content) throws IllegalArgumentException {
Assert.checkNotNullParam("target", target);
Assert.checkNotNullParam("type", type);
Assert.checkNotNullParam("content", content);
final Matcher matcher = VALID_LABEL.matcher(type);
if (matcher.find()) {
throw log.invalidPemType("<any valid PEM type>", type);
}
target.append("-----BEGIN ").append(type).append("-----\n");
target.append(content.base64Encode().drainToString('\n', 64));
target.append("\n-----END ").append(type).append("-----\n");
}
/**
* Extracts the DER content from the given <code>pemContent</code>.
*
* @param pemContent a {@link CodePointIterator} with the PEM content
* @return a byte array with the DER content
*/
public static byte[] extractDerContent(CodePointIterator pemContent) {
return parsePemContent(pemContent, new BiFunction<String, ByteIterator, byte[]>() {
@Override
public byte[] apply(String type, ByteIterator byteIterator) {
return byteIterator.drain();
}
});
}
private static X509Certificate parsePemX509CertificateContent(String type, ByteIterator byteIterator) throws IllegalArgumentException {
if (! type.equals(CERTIFICATE_FORMAT)) {
throw log.invalidPemType(CERTIFICATE_FORMAT, type);
}
try {
final CertificateFactory instance = CertificateFactory.getInstance("X.509");
return (X509Certificate) instance.generateCertificate(byteIterator.asInputStream());
} catch (CertificateException e) {
throw log.certificateParseError(e);
}
}
private static PublicKey parsePemPublicKey(String type, ByteIterator byteIterator) throws IllegalArgumentException {
if (! type.equals(PUBLIC_KEY_FORMAT)) {
throw log.invalidPemType(PUBLIC_KEY_FORMAT, type);
}
try {
byte[] der = byteIterator.drain();
DERDecoder derDecoder = new DERDecoder(der);
derDecoder.startSequence();
switch (derDecoder.peekType()) {
case ASN1.SEQUENCE_TYPE:
derDecoder.startSequence();
String algorithm = derDecoder.decodeObjectIdentifierAsKeyAlgorithm();
if (algorithm != null) {
return KeyFactory.getInstance(algorithm).generatePublic(new X509EncodedKeySpec(der));
}
throw log.asnUnrecognisedAlgorithm(algorithm);
default:
throw log.asnUnexpectedTag();
}
} catch (Exception cause) {
throw log.publicKeyParseError(cause);
}
}
/**
* Parse an X.509 certificate in PEM format.
*
* @param pemContent the PEM content (must not be {@code null})
* @return the certificate (not {@code null})
* @throws IllegalArgumentException if the certificate could not be parsed for some reason
*/
public static X509Certificate parsePemX509Certificate(CodePointIterator pemContent) throws IllegalArgumentException {
Assert.checkNotNullParam("pemContent", pemContent);
return parsePemContent(pemContent, Pem::parsePemX509CertificateContent);
}
/**
* Parse a {@link PublicKey} in PEM format.
*
* @param pemContent the PEM content (must not be {@code null})
* @return the public key (not {@code null})
* @throws IllegalArgumentException if the public key could not be parsed for some reason
*/
public static PublicKey parsePemPublicKey(CodePointIterator pemContent) throws IllegalArgumentException {
Assert.checkNotNullParam("pemContent", pemContent);
return parsePemContent(pemContent, Pem::parsePemPublicKey);
}
/**
* Generate PEM content containing an X.509 certificate.
*
* @param target the target byte string builder (must not be {@code null})
* @param certificate the X.509 certificate (must not be {@code null})
*/
public static void generatePemX509Certificate(ByteStringBuilder target, X509Certificate certificate) {
Assert.checkNotNullParam("target", target);
Assert.checkNotNullParam("certificate", certificate);
try {
generatePemContent(target, CERTIFICATE_FORMAT, ByteIterator.ofBytes(certificate.getEncoded()));
} catch (CertificateEncodingException e) {
throw log.certificateParseError(e);
}
}
/**
* Generate PEM content containing a {@link PublicKey}.
*
* @param target the target byte string builder (must not be {@code null})
* @param publicKey the {@link PublicKey} (must not be {@code null})
*/
public static void generatePemPublicKey(ByteStringBuilder target, PublicKey publicKey) {
Assert.checkNotNullParam("target", target);
Assert.checkNotNullParam("publicKey", publicKey);
try {
KeyFactory instance = KeyFactory.getInstance(publicKey.getAlgorithm());
X509EncodedKeySpec keySpec = instance.getKeySpec(publicKey, X509EncodedKeySpec.class);
generatePemContent(target, PUBLIC_KEY_FORMAT, ByteIterator.ofBytes(keySpec.getEncoded()));
} catch (Exception e) {
throw log.publicKeyParseError(e);
}
}
}