All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.shredzone.acme4j.toolbox.AcmeUtils Maven / Gradle / Ivy

There is a newer version: 3.4.0
Show newest version
/*
 * acme4j - Java ACME client
 *
 * Copyright (C) 2016 Richard "Shred" Körber
 *   http://acme4j.shredzone.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 *
 * 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.
 */
package org.shredzone.acme4j.toolbox;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.IOException;
import java.io.Writer;
import java.net.IDN;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Base64;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import edu.umd.cs.findbugs.annotations.Nullable;
import org.shredzone.acme4j.exception.AcmeProtocolException;

/**
 * Contains utility methods that are frequently used for the ACME protocol.
 * 

* This class is internal. You may use it in your own code, but be warned that methods may * change their signature or disappear without prior announcement. */ public final class AcmeUtils { private static final char[] HEX = "0123456789abcdef".toCharArray(); private static final String ACME_ERROR_PREFIX = "urn:ietf:params:acme:error:"; private static final Pattern DATE_PATTERN = Pattern.compile( "^(\\d{4})-(\\d{2})-(\\d{2})T" + "(\\d{2}):(\\d{2}):(\\d{2})" + "(?:\\.(\\d{1,3})\\d*)?" + "(Z|[+-]\\d{2}:?\\d{2})$", Pattern.CASE_INSENSITIVE); private static final Pattern TZ_PATTERN = Pattern.compile( "([+-])(\\d{2}):?(\\d{2})$"); private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile( "([^;]+)(?:;.*?charset=(\"?)([a-z0-9_-]+)(\\2))?.*", Pattern.CASE_INSENSITIVE); private static final Pattern MAIL_PATTERN = Pattern.compile("\\?|@.*,"); private static final Pattern BASE64URL_PATTERN = Pattern.compile("[0-9A-Za-z_-]*"); private static final Base64.Encoder PEM_ENCODER = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.US_ASCII)); private static final Base64.Encoder URL_ENCODER = Base64.getUrlEncoder().withoutPadding(); private static final Base64.Decoder URL_DECODER = Base64.getUrlDecoder(); /** * Enumeration of PEM labels. */ public enum PemLabel { CERTIFICATE("CERTIFICATE"), CERTIFICATE_REQUEST("CERTIFICATE REQUEST"), PRIVATE_KEY("PRIVATE KEY"), PUBLIC_KEY("PUBLIC KEY"); private final String label; PemLabel(String label) { this.label = label; } @Override public String toString() { return label; } } private AcmeUtils() { // Utility class without constructor } /** * Computes a SHA-256 hash of the given string. * * @param z * String to hash * @return Hash */ public static byte[] sha256hash(String z) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update(z.getBytes(UTF_8)); return md.digest(); } catch (NoSuchAlgorithmException ex) { throw new AcmeProtocolException("Could not compute hash", ex); } } /** * Hex encodes the given byte array. * * @param data * byte array to hex encode * @return Hex encoded string of the data (with lower case characters) */ public static String hexEncode(byte[] data) { char[] result = new char[data.length * 2]; for (int ix = 0; ix < data.length; ix++) { int val = data[ix] & 0xFF; result[ix * 2] = HEX[val >>> 4]; result[ix * 2 + 1] = HEX[val & 0x0F]; } return new String(result); } /** * Base64 encodes the given byte array, using URL style encoding. * * @param data * byte array to base64 encode * @return base64 encoded string */ public static String base64UrlEncode(byte[] data) { return URL_ENCODER.encodeToString(data); } /** * Base64 decodes to a byte array, using URL style encoding. * * @param base64 * base64 encoded string * @return decoded data */ public static byte[] base64UrlDecode(String base64) { return URL_DECODER.decode(base64); } /** * Validates that the given {@link String} is a valid base64url encoded value. * * @param base64 * {@link String} to validate * @return {@code true}: String contains a valid base64url encoded value. * {@code false} if the {@link String} was {@code null} or contained illegal * characters. * @since 2.6 */ public static boolean isValidBase64Url(@Nullable String base64) { return base64 != null && BASE64URL_PATTERN.matcher(base64).matches(); } /** * ASCII encodes a domain name. *

* The conversion is done as described in * RFC 3490. Additionally, all * leading and trailing white spaces are trimmed, and the result is lowercased. *

* It is safe to pass in ACE encoded domains, they will be returned unchanged. * * @param domain * Domain name to encode * @return Encoded domain name, white space trimmed and lower cased. */ public static String toAce(String domain) { Objects.requireNonNull(domain, "domain"); return IDN.toASCII(domain.trim()).toLowerCase(); } /** * Parses a RFC 3339 formatted date. * * @param str * Date string * @return {@link Instant} that was parsed * @throws IllegalArgumentException * if the date string was not RFC 3339 formatted * @see RFC 3339 */ public static Instant parseTimestamp(String str) { Matcher m = DATE_PATTERN.matcher(str); if (!m.matches()) { throw new IllegalArgumentException("Illegal date: " + str); } int year = Integer.parseInt(m.group(1)); int month = Integer.parseInt(m.group(2)); int dom = Integer.parseInt(m.group(3)); int hour = Integer.parseInt(m.group(4)); int minute = Integer.parseInt(m.group(5)); int second = Integer.parseInt(m.group(6)); StringBuilder msStr = new StringBuilder(); if (m.group(7) != null) { msStr.append(m.group(7)); } while (msStr.length() < 3) { msStr.append('0'); } int ms = Integer.parseInt(msStr.toString()); String tz = m.group(8); if ("Z".equalsIgnoreCase(tz)) { tz = "GMT"; } else { tz = TZ_PATTERN.matcher(tz).replaceAll("GMT$1$2:$3"); } return ZonedDateTime.of( year, month, dom, hour, minute, second, ms * 1_000_000, ZoneId.of(tz)).toInstant(); } /** * Strips the acme error prefix from the error string. *

* For example, for "urn:ietf:params:acme:error:unauthorized", "unauthorized" is * returned. * * @param type * Error type to strip the prefix from. {@code null} is safe. * @return Stripped error type, or {@code null} if the prefix was not found. */ @Nullable public static String stripErrorPrefix(@Nullable String type) { if (type != null && type.startsWith(ACME_ERROR_PREFIX)) { return type.substring(ACME_ERROR_PREFIX.length()); } else { return null; } } /** * Writes an encoded key or certificate to a file in PEM format. * * @param encoded * Encoded data to write * @param label * {@link PemLabel} to be used * @param out * {@link Writer} to write to. It will not be closed after use! */ public static void writeToPem(byte[] encoded, PemLabel label, Writer out) throws IOException { out.append("-----BEGIN ").append(label.toString()).append("-----\n"); out.append(new String(PEM_ENCODER.encode(encoded), StandardCharsets.US_ASCII)); out.append("\n-----END ").append(label.toString()).append("-----\n"); } /** * Extracts the content type of a Content-Type header. * * @param header * Content-Type header * @return Content-Type, or {@code null} if the header was invalid or empty * @throws AcmeProtocolException * if the Content-Type header contains a different charset than "utf-8". */ @Nullable public static String getContentType(@Nullable String header) { if (header != null) { Matcher m = CONTENT_TYPE_PATTERN.matcher(header); if (m.matches()) { String charset = m.group(3); if (charset != null && !"utf-8".equalsIgnoreCase(charset)) { throw new AcmeProtocolException("Unsupported charset " + charset); } return m.group(1).trim().toLowerCase(); } } return null; } /** * Validates a contact {@link URI}. * * @param contact * Contact {@link URI} to validate * @throws IllegalArgumentException * if the contact {@link URI} is not suitable for account contacts. */ public static void validateContact(URI contact) { if ("mailto".equalsIgnoreCase(contact.getScheme())) { String address = contact.toString().substring(7); if (MAIL_PATTERN.matcher(address).find()) { throw new IllegalArgumentException( "multiple recipients or hfields are not allowed: " + contact); } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy