NIFI-11780 Added Certificate SAN Attributes to HandleHttpRequest

Signed-off-by: Pierre Villard <pierre.villard.fr@gmail.com>

This closes #7521.
This commit is contained in:
exceptionfactory 2023-07-21 13:04:48 -05:00 committed by Pierre Villard
parent 72618b1817
commit 3a078c0c31
No known key found for this signature in database
GPG Key ID: F92A93B30C07C6D5
5 changed files with 370 additions and 11 deletions

View File

@ -41,6 +41,9 @@ import org.apache.nifi.processor.ProcessSession;
import org.apache.nifi.processor.Relationship;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.processor.util.StandardValidators;
import org.apache.nifi.processors.standard.http.CertificateAttribute;
import org.apache.nifi.processors.standard.http.CertificateAttributesProvider;
import org.apache.nifi.processors.standard.http.HandleHttpRequestCertificateAttributesProvider;
import org.apache.nifi.processors.standard.http.HttpProtocolStrategy;
import org.apache.nifi.processors.standard.util.HTTPUtils;
import org.apache.nifi.scheduling.ExecutionNode;
@ -69,7 +72,6 @@ import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
@ -122,6 +124,10 @@ import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
+ "unless the Processor is configured to use an SSLContext Service"),
@WritesAttribute(attribute = "http.issuer.dn", description = "The Distinguished Name of the entity that issued the Subject's certificate. "
+ "This value will not be populated unless the Processor is configured to use an SSLContext Service"),
@WritesAttribute(attribute = "http.certificate.sans.N.name", description = "X.509 Client Certificate Subject Alternative Name value from mutual TLS authentication. "
+ "The attribute name has a zero-based index ordered according to the content of Client Certificate"),
@WritesAttribute(attribute = "http.certificate.sans.N.nameType", description = "X.509 Client Certificate Subject Alternative Name type from mutual TLS authentication. "
+ "The attribute name has a zero-based index ordered according to the content of Client Certificate. The attribute value is one of the General Names from RFC 3280 Section 4.1.2.7"),
@WritesAttribute(attribute = "http.headers.XXX", description = "Each of the HTTP Headers that is received in the request will be added as an "
+ "attribute, prefixed with \"http.headers.\" For example, if the request contains an HTTP Header named \"x-my-header\", then the value "
+ "will be added to an attribute named \"http.headers.x-my-header\""),
@ -343,6 +349,7 @@ public class HandleHttpRequest extends AbstractProcessor {
private final AtomicBoolean initialized = new AtomicBoolean(false);
private final AtomicBoolean runOnPrimary = new AtomicBoolean(false);
private final AtomicReference<Set<String>> parameterToAttributesReference = new AtomicReference<>(null);
private final CertificateAttributesProvider certificateAttributesProvider = new HandleHttpRequestCertificateAttributesProvider();
@Override
protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
@ -756,22 +763,15 @@ public class HandleHttpRequest extends AbstractProcessor {
putAttribute(attributes, "http.principal.name", principal.getName());
}
final X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
if (certs != null && certs.length > 0) {
final X509Certificate cert = certs[0];
final String subjectDn = cert.getSubjectDN().getName();
final String issuerDn = cert.getIssuerDN().getName();
putAttribute(attributes, HTTPUtils.HTTP_SSL_CERT, subjectDn);
putAttribute(attributes, "http.issuer.dn", issuerDn);
}
final Map<String, String> certificateAttributes = certificateAttributesProvider.getCertificateAttributes(request);
attributes.putAll(certificateAttributes);
return session.putAllAttributes(flowFile, attributes);
}
private void forwardFlowFile(final ProcessSession session, final long start, final HttpServletRequest request, final FlowFile flowFile) {
final long receiveMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
final String subjectDn = flowFile.getAttribute(HTTPUtils.HTTP_SSL_CERT);
final String subjectDn = flowFile.getAttribute(CertificateAttribute.HTTP_SUBJECT_DN.getName());
session.getProvenanceReporter().receive(flowFile, HTTPUtils.getURI(flowFile.getAttributes()),
"Received from " + request.getRemoteAddr() + (subjectDn == null ? "" : " with DN=" + subjectDn), receiveMillis);
session.transfer(flowFile, REL_SUCCESS);

View File

@ -0,0 +1,44 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.nifi.processors.standard.http;
/**
* X.509 Client Certificate FlowFile Attribute Names
*/
public enum CertificateAttribute {
/** Certificate Subject Distinguished Name */
HTTP_SUBJECT_DN("http.subject.dn"),
/** Certificate Issuer Distinguished Name */
HTTP_ISSUER_DN("http.issuer.dn"),
/** Certificate Subject Distinguished Name */
HTTP_CERTIFICATE_PARSING_EXCEPTION("http.certificate.parsing.exception"),
/** Certificate Subject Alternative Names */
HTTP_CERTIFICATE_SANS("http.certificate.sans");
private final String name;
CertificateAttribute(final String name) {
this.name = name;
}
public String getName() {
return name;
}
}

View File

@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.nifi.processors.standard.http;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* Provider abstraction for reading X.509 Client Certificates from an HTTP Servlet Request and returning FlowFile attributes
*/
public interface CertificateAttributesProvider {
/**
* Get X.509 Client Certificate Attributes
*
* @param request HTTP Servlet Request
* @return Map of Client Certificate Attributes or empty when no client certificate found
*/
Map<String, String> getCertificateAttributes(HttpServletRequest request);
}

View File

@ -0,0 +1,131 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.nifi.processors.standard.http;
import javax.servlet.http.HttpServletRequest;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Certificate Attributes Provider for HandleHttpRequest reads the first X.509 Certificate presented
*/
public class HandleHttpRequestCertificateAttributesProvider implements CertificateAttributesProvider {
protected static final String REQUEST_CERTIFICATES_ATTRIBUTE_NAME = "javax.servlet.request.X509Certificate";
private static final String SAN_NAME_TYPE_FORMAT = "%s.%d.nameType";
private static final String SAN_NAME_FORMAT = "%s.%d.name";
private static final Map<String, String> GENERAL_NAME_TYPES = new LinkedHashMap<>();
static {
// General Name types defined in RFC 3280 Section 4.2.1.7 */
GENERAL_NAME_TYPES.put("0", "otherName");
GENERAL_NAME_TYPES.put("1", "rfc822Name");
GENERAL_NAME_TYPES.put("2", "dNSName");
GENERAL_NAME_TYPES.put("3", "x400Address");
GENERAL_NAME_TYPES.put("4", "directoryName");
GENERAL_NAME_TYPES.put("5", "ediPartyName");
GENERAL_NAME_TYPES.put("6", "uniformResourceIdentifier");
GENERAL_NAME_TYPES.put("7", "iPAddress");
GENERAL_NAME_TYPES.put("8", "registeredID");
}
@Override
public Map<String, String> getCertificateAttributes(final HttpServletRequest request) {
Objects.requireNonNull(request, "HTTP Servlet Request required");
final Map<String, String> attributes;
final Object requestCertificates = request.getAttribute(REQUEST_CERTIFICATES_ATTRIBUTE_NAME);
if (requestCertificates instanceof X509Certificate[]) {
final X509Certificate[] certificates = (X509Certificate[]) requestCertificates;
if (certificates.length == 0) {
attributes = Collections.emptyMap();
} else {
final X509Certificate clientCertificate = certificates[0];
attributes = getCertificateAttributes(clientCertificate);
}
} else {
attributes = Collections.emptyMap();
}
return attributes;
}
private Map<String, String> getCertificateAttributes(final X509Certificate certificate) {
final Map<String, String> attributes = new LinkedHashMap<>();
final String subjectPrincipal = certificate.getSubjectX500Principal().getName();
final String issuerPrincipal = certificate.getIssuerX500Principal().getName();
attributes.put(CertificateAttribute.HTTP_SUBJECT_DN.getName(), subjectPrincipal);
attributes.put(CertificateAttribute.HTTP_ISSUER_DN.getName(), issuerPrincipal);
try {
final Collection<List<?>> subjectAlternativeNames = certificate.getSubjectAlternativeNames();
if (subjectAlternativeNames != null) {
final Map<String, String> subjectAlternativeNameAttributes = getSubjectAlternativeNameAttributes(subjectAlternativeNames);
attributes.putAll(subjectAlternativeNameAttributes);
}
} catch (final CertificateParsingException e) {
attributes.put(CertificateAttribute.HTTP_CERTIFICATE_PARSING_EXCEPTION.getName(), e.getMessage());
}
return attributes;
}
private Map<String, String> getSubjectAlternativeNameAttributes(final Collection<List<?>> subjectAlternativeNames) {
final Map<String, String> attributes = new LinkedHashMap<>();
int subjectAlternativeNameIndex = 0;
for (final List<?> subjectAlternativeTypeName : subjectAlternativeNames) {
final String nameTypeAttributeKey = String.format(SAN_NAME_TYPE_FORMAT, CertificateAttribute.HTTP_CERTIFICATE_SANS.getName(), subjectAlternativeNameIndex);
final String nameType = subjectAlternativeTypeName.get(0).toString();
final String generalNameType = GENERAL_NAME_TYPES.getOrDefault(nameType, nameType);
attributes.put(nameTypeAttributeKey, generalNameType);
final String nameAttributeKey = String.format(SAN_NAME_FORMAT, CertificateAttribute.HTTP_CERTIFICATE_SANS.getName(), subjectAlternativeNameIndex);
final Object name = subjectAlternativeTypeName.get(1);
final String serializedName = getSerializedName(name);
attributes.put(nameAttributeKey, serializedName);
subjectAlternativeNameIndex++;
}
return attributes;
}
private String getSerializedName(final Object name) {
final String serializedName;
if (name instanceof byte[]) {
final byte[] encodedName = (byte[]) name;
serializedName = Base64.getEncoder().encodeToString(encodedName);
} else {
serializedName = name.toString();
}
return serializedName;
}
}

View File

@ -0,0 +1,151 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.nifi.processors.standard.http;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import javax.security.auth.x500.X500Principal;
import javax.servlet.http.HttpServletRequest;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class HandleHttpRequestCertificateAttributesProviderTest {
private static final X500Principal SUBJECT_PRINCIPAL = new X500Principal("CN=subject, OU=users");
private static final X500Principal ISSUER_PRINCIPAL = new X500Principal("CN=issuer, OU=authorities");
private static final String RFC_822_NAME_GENERAL_NAME = "rfc822Name";
private static final String DNS_NAME_GENERAL_NAME = "dNSName";
private static final Integer RFC_822_NAME_TYPE = 1;
private static final String EMAIL_ADDRESS = "username@localhost.localdomain";
private static final Integer DNS_NAME_TYPE = 2;
private static final String DNS_NAME = "localhost.localdomain";
private static final String FIRST_SAN_NAME_ATTRIBUTE_KEY = "http.certificate.sans.0.name";
private static final String FIRST_SAN_NAME_TYPE_ATTRIBUTE_KEY = "http.certificate.sans.0.nameType";
private static final String SECOND_SAN_NAME_ATTRIBUTE_KEY = "http.certificate.sans.1.name";
private static final String SECOND_SAN_NAME_TYPE_ATTRIBUTE_KEY = "http.certificate.sans.1.nameType";
private static final String PARSING_EXCEPTION_MESSAGE = "SAN parsing failed";
@Mock
private HttpServletRequest request;
@Mock
private X509Certificate certificate;
private HandleHttpRequestCertificateAttributesProvider provider;
@BeforeEach
void setProvider() {
provider = new HandleHttpRequestCertificateAttributesProvider();
}
@Test
void testCertificatesNotFound() {
final Map<String, String> attributes = provider.getCertificateAttributes(request);
assertTrue(attributes.isEmpty());
}
@Test
void testCertificatesFound() {
final X509Certificate[] certificates = new X509Certificate[]{certificate};
when(request.getAttribute(eq(HandleHttpRequestCertificateAttributesProvider.REQUEST_CERTIFICATES_ATTRIBUTE_NAME))).thenReturn(certificates);
when(certificate.getSubjectX500Principal()).thenReturn(SUBJECT_PRINCIPAL);
when(certificate.getIssuerX500Principal()).thenReturn(ISSUER_PRINCIPAL);
final Map<String, String> attributes = provider.getCertificateAttributes(request);
assertSubjectIssuerFound(attributes);
}
@Test
void testCertificatesFoundParsingException() throws CertificateParsingException {
final X509Certificate[] certificates = new X509Certificate[]{certificate};
when(request.getAttribute(eq(HandleHttpRequestCertificateAttributesProvider.REQUEST_CERTIFICATES_ATTRIBUTE_NAME))).thenReturn(certificates);
when(certificate.getSubjectX500Principal()).thenReturn(SUBJECT_PRINCIPAL);
when(certificate.getIssuerX500Principal()).thenReturn(ISSUER_PRINCIPAL);
when(certificate.getSubjectAlternativeNames()).thenThrow(new CertificateParsingException(PARSING_EXCEPTION_MESSAGE));
final Map<String, String> attributes = provider.getCertificateAttributes(request);
assertSubjectIssuerFound(attributes);
assertEquals(attributes.get(CertificateAttribute.HTTP_CERTIFICATE_PARSING_EXCEPTION.getName()), PARSING_EXCEPTION_MESSAGE);
}
@Test
void testCertificateSubjectAlternativeNamesFound() throws CertificateParsingException {
final X509Certificate[] certificates = new X509Certificate[]{certificate};
when(request.getAttribute(eq(HandleHttpRequestCertificateAttributesProvider.REQUEST_CERTIFICATES_ATTRIBUTE_NAME))).thenReturn(certificates);
when(certificate.getSubjectX500Principal()).thenReturn(SUBJECT_PRINCIPAL);
when(certificate.getIssuerX500Principal()).thenReturn(ISSUER_PRINCIPAL);
final Collection<List<?>> subjectAlternativeNames = new ArrayList<>();
final List<?> emailAddressName = Arrays.asList(RFC_822_NAME_TYPE, EMAIL_ADDRESS);
subjectAlternativeNames.add(emailAddressName);
final List<?> dnsName = Arrays.asList(DNS_NAME_TYPE, DNS_NAME);
subjectAlternativeNames.add(dnsName);
when(certificate.getSubjectAlternativeNames()).thenReturn(subjectAlternativeNames);
final Map<String, String> attributes = provider.getCertificateAttributes(request);
assertSubjectIssuerFound(attributes);
assertEquals(attributes.get(FIRST_SAN_NAME_ATTRIBUTE_KEY), EMAIL_ADDRESS);
assertEquals(attributes.get(FIRST_SAN_NAME_TYPE_ATTRIBUTE_KEY), RFC_822_NAME_GENERAL_NAME);
assertEquals(attributes.get(SECOND_SAN_NAME_ATTRIBUTE_KEY), DNS_NAME);
assertEquals(attributes.get(SECOND_SAN_NAME_TYPE_ATTRIBUTE_KEY), DNS_NAME_GENERAL_NAME);
}
private void assertSubjectIssuerFound(final Map<String, String> attributes) {
assertEquals(SUBJECT_PRINCIPAL.getName(), attributes.get(CertificateAttribute.HTTP_SUBJECT_DN.getName()));
assertEquals(ISSUER_PRINCIPAL.getName(), attributes.get(CertificateAttribute.HTTP_ISSUER_DN.getName()));
}
}