// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.security.tls; import com.yahoo.security.SubjectAlternativeName; import com.yahoo.security.X509CertificateUtils; import java.security.cert.X509Certificate; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.logging.Logger; import java.util.stream.Collectors; import static com.yahoo.security.SubjectAlternativeName.Type.DNS; import static com.yahoo.security.SubjectAlternativeName.Type.URI; import static com.yahoo.security.tls.CapabilityMode.DISABLE; import static com.yahoo.security.tls.CapabilityMode.LOG_ONLY; /** * @author bjorncs */ public record ConnectionAuthContext(List peerCertificateChain, CapabilitySet capabilities, Set matchedPolicies, CapabilityMode capabilityMode) { private static final Logger log = Logger.getLogger(ConnectionAuthContext.class.getName()); public ConnectionAuthContext { peerCertificateChain = List.copyOf(peerCertificateChain); matchedPolicies = Set.copyOf(matchedPolicies); } private ConnectionAuthContext(List certs, CapabilityMode capabilityMode) { this(certs, CapabilitySet.all(), Set.of(), capabilityMode); } public boolean authorized() { return !capabilities.hasNone(); } /** Throws checked exception to force caller to handle verification failed. */ public void verifyCapabilities(CapabilitySet requiredCapabilities) throws MissingCapabilitiesException { verifyCapabilities(requiredCapabilities, null, null, null); } /** * Throws checked exception to force caller to handle verification failed. * Provided strings are used for improved logging only * */ public void verifyCapabilities(CapabilitySet requiredCapabilities, String action, String resource, String peer) throws MissingCapabilitiesException { if (capabilityMode == DISABLE) return; boolean hasCapabilities = capabilities.has(requiredCapabilities); if (!hasCapabilities) { TlsMetrics.instance().incrementCapabilitiesFailed(); String msg = createPermissionDeniedErrorMessage(requiredCapabilities, action, resource, peer); if (capabilityMode == LOG_ONLY) { log.info(msg); } else { // Ideally log as warning, but we have no mechanism for de-duplicating repeated log spamming. log.fine(msg); throw new MissingCapabilitiesException(msg); } } else { TlsMetrics.instance().incrementCapabilitiesSucceeded(); } } String createPermissionDeniedErrorMessage( CapabilitySet required, String action, String resource, String peer) { StringBuilder b = new StringBuilder(); if (capabilityMode == LOG_ONLY) b.append("Dry-run: "); b.append("Permission denied"); if (resource != null) { b.append(" for '"); if (action != null) { b.append(action).append("' on '"); } b.append(resource).append("'"); } b.append(". Peer "); if (peer != null) b.append("'").append(peer).append("' "); return b.append("with ").append(peerCertificateString().orElse("")).append(". Requires capabilities ") .append(toCapabilityNames(required)).append(" but peer has ").append(toCapabilityNames(capabilities)) .append(".").toString(); } private static String toCapabilityNames(CapabilitySet capabilities) { return capabilities.toCapabilityNames().stream().sorted().collect(Collectors.joining(", ", "[", "]")); } public Optional peerCertificate() { return peerCertificateChain.isEmpty() ? Optional.empty() : Optional.of(peerCertificateChain.get(0)); } public Optional peerCertificateString() { X509Certificate cert = peerCertificate().orElse(null); if (cert == null) return Optional.empty(); StringBuilder b = new StringBuilder("["); String cn = X509CertificateUtils.getSubjectCommonName(cert).orElse(null); if (cn != null) { b.append("CN='").append(cn).append("'"); } var sans = X509CertificateUtils.getSubjectAlternativeNames(cert); List dnsNames = sans.stream() .filter(s -> s.getType() == DNS) .map(SubjectAlternativeName::getValue) .toList(); if (!dnsNames.isEmpty()) { if (cn != null) b.append(", "); b.append("SAN_DNS=").append(dnsNames); } List uris = sans.stream() .filter(s -> s.getType() == URI) .map(SubjectAlternativeName::getValue) .toList(); if (!uris.isEmpty()) { if (cn != null || !dnsNames.isEmpty()) b.append(", "); b.append("SAN_URI=").append(uris); } return Optional.of(b.append("]").toString()); } /** Construct instance with all capabilities */ public static ConnectionAuthContext defaultAllCapabilities() { return new ConnectionAuthContext(List.of(), DISABLE); } /** Construct instance with all capabilities */ public static ConnectionAuthContext defaultAllCapabilities(List certs) { return new ConnectionAuthContext(certs, DISABLE); } }