blob: bf8f1519fe157c3a264261410f52d8dc070fa4b6 [file]
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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 General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import static jdk.jpackage.internal.MacCertificateUtils.findCertificates;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.security.auth.x500.X500Principal;
import jdk.jpackage.internal.MacCertificateUtils.CertificateHash;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.SigningIdentity;
final class SigningIdentityBuilder {
static class SigningConfigException extends ConfigException {
SigningConfigException(ConfigException ex) {
super(ex.getMessage(), ex.getAdvice(), ex.getCause());
}
private static final long serialVersionUID = 1L;
}
static class ExpiredCertificateException extends SigningConfigException {
ExpiredCertificateException(ConfigException ex) {
super(ex);
}
private static final long serialVersionUID = 1L;
}
record SigningConfig(SigningIdentity identity, Optional<Keychain> keychain) {
SigningConfig {
Objects.requireNonNull(identity);
Objects.requireNonNull(keychain);
}
}
SigningIdentityBuilder signingIdentity(String v) {
signingIdentity = v;
return this;
}
SigningIdentityBuilder certificateSelector(CertificateSelector v) {
certificateSelector = v;
return this;
}
SigningIdentityBuilder keychain(String v) {
keychain = v;
return this;
}
Optional<SigningConfig> create() throws ConfigException {
if (signingIdentity == null && certificateSelector == null) {
return Optional.empty();
} else {
return Optional.of(new SigningConfig(validatedSigningIdentity(), validatedKeychain()));
}
}
private Optional<Keychain> validatedKeychain() throws ConfigException {
return Optional.ofNullable(keychain).map(Keychain::new);
}
private SigningIdentity validatedSigningIdentity() throws ConfigException {
if (signingIdentity != null) {
return new SigningIdentityImpl(signingIdentity);
}
Objects.requireNonNull(certificateSelector);
final var validatedKeychain = validatedKeychain();
final var allCertificates = findCertificates(validatedKeychain, Optional.empty());
final var mappedCertficates = allCertificates.stream().<Map.Entry<String, X509Certificate>>mapMulti((cert, acc) -> {
findSubjectCNs(cert).stream().map(cn -> {
return Map.entry(cn, cert);
}).forEach(acc::accept);
}).toList();
final var signingIdentityNames = certificateSelector.signingIdentities();
// Look up for the exact match.
var matchingCertificates = mappedCertficates.stream().filter(e -> {
return signingIdentityNames.contains(e.getKey());
}).map(Map.Entry::getValue).toList();
if (matchingCertificates.isEmpty()) {
// No exact matches found, look up for substrings.
matchingCertificates = mappedCertficates.stream().filter(e -> {
return signingIdentityNames.stream().anyMatch(filter -> {
return e.getKey().startsWith(filter);
});
}).map(Map.Entry::getValue).toList();
}
final var cert = selectSigningIdentity(matchingCertificates,
certificateSelector, validatedKeychain);
try {
cert.checkValidity();
} catch (CertificateExpiredException|CertificateNotYetValidException ex) {
throw new ExpiredCertificateException(I18N.buildConfigException("error.certificate.expired", findSubjectCNs(cert).getFirst()).create());
}
final var signingIdentityHash = CertificateHash.of(cert);
return new SigningIdentityImpl(signingIdentityHash.toString());
}
private static X509Certificate selectSigningIdentity(List<X509Certificate> certs,
CertificateSelector certificateSelector, Optional<Keychain> keychain) throws ConfigException {
Objects.requireNonNull(certificateSelector);
Objects.requireNonNull(keychain);
switch (certs.size()) {
case 0 -> {
Log.error(I18N.format("error.cert.not.found", certificateSelector.signingIdentities().getFirst(),
keychain.map(Keychain::name).orElse("")));
throw I18N.buildConfigException("error.explicit-sign-no-cert")
.advice("error.explicit-sign-no-cert.advice").create();
}
case 1 -> {
return certs.getFirst();
}
default -> {
Log.error(I18N.format("error.multiple.certs.found", certificateSelector.signingIdentities().getFirst(),
keychain.map(Keychain::name).orElse("")));
return certs.getFirst();
}
}
}
private static List<String> findSubjectCNs(X509Certificate cert) {
final LdapName ldapName;
try {
ldapName = new LdapName(cert.getSubjectX500Principal().getName(X500Principal.RFC2253));
} catch (InvalidNameException e) {
return List.of();
}
return ldapName.getRdns().stream().filter(rdn -> {
return rdn.getType().equalsIgnoreCase("CN");
}).map(Rdn::getValue).map(Object::toString).distinct().toList();
}
record CertificateSelector(String name, List<StandardCertificatePrefix> prefixes) {
CertificateSelector {
Objects.requireNonNull(prefixes);
prefixes.forEach(Objects::requireNonNull);
Objects.requireNonNull(name);
}
List<String> signingIdentities() {
if (prefixes().isEmpty()) {
return List.of(name);
} else {
return prefixes.stream().map(StandardCertificatePrefix::value).map(prefix -> {
return prefix + name;
}).toList();
}
}
}
enum StandardCertificatePrefix {
APP_SIGN_APP_STORE("3rd Party Mac Developer Application"),
INSTALLER_SIGN_APP_STORE("3rd Party Mac Developer Installer"),
APP_SIGN_PERSONAL("Developer ID Application"),
INSTALLER_SIGN_PERSONAL("Developer ID Installer");
StandardCertificatePrefix(String value) {
this.value = value + ": ";
}
String value() {
return value;
}
static Optional<StandardCertificatePrefix> findStandardCertificatePrefix(String certificateLocator) {
Objects.requireNonNull(certificateLocator);
return Stream.of(StandardCertificatePrefix.values()).filter(prefix -> {
return certificateLocator.startsWith(prefix.value);
}).reduce((x, y) -> {
throw new UnsupportedOperationException();
});
}
private final String value;
}
enum StandardCertificateSelector {
APP_IMAGE(StandardCertificatePrefix.APP_SIGN_PERSONAL),
PKG_INSTALLER(StandardCertificatePrefix.INSTALLER_SIGN_PERSONAL),
APP_STORE_APP_IMAGE(StandardCertificatePrefix.APP_SIGN_PERSONAL, StandardCertificatePrefix.APP_SIGN_APP_STORE),
APP_STORE_PKG_INSTALLER(StandardCertificatePrefix.INSTALLER_SIGN_PERSONAL, StandardCertificatePrefix.INSTALLER_SIGN_APP_STORE);
StandardCertificateSelector(StandardCertificatePrefix ... prefixes) {
this.prefixes = List.of(prefixes);
}
static CertificateSelector create(String certificateLocator, StandardCertificateSelector defaultSelector) {
return StandardCertificatePrefix.findStandardCertificatePrefix(certificateLocator).map(prefix -> {
return new CertificateSelector(certificateLocator, List.of());
}).orElseGet(() -> {
return new CertificateSelector(certificateLocator, defaultSelector.prefixes);
});
}
private final List<StandardCertificatePrefix> prefixes;
}
private String signingIdentity;
private CertificateSelector certificateSelector;
private String keychain;
}