ttomcat-1778514358873.zip-extract/apache-tomcat-11.0.18-src/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java

Path
ttomcat-1778514358873.zip-extract/apache-tomcat-11.0.18-src/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java
Status
scanned
Type
file
Name
OpenSSLContext.java
Extension
.java
Programming language
Java
Mime type
text/plain
File type
ASCII text, with CRLF line terminators
Tag

      
    
Rootfs path

      
    
Size
28926 (28.2 KB)
MD5
57ccb26241b638fcdb892a1ad9bc2667
SHA1
c1755677fe439393cc32078a7255de5f1fcd04d7
SHA256
ed346760e6ef1fcfe914f3bab925f38984a13e58a28cfb0099cf03c127c8e47a
SHA512

      
    
SHA1_git
0b3ab15c72f21fac53ab54d6ea7e99dc8b10c8e7
Is binary

      
    
Is text
True
Is archive

      
    
Is media

      
    
Is legal

      
    
Is manifest

      
    
Is readme

      
    
Is top level

      
    
Is key file

      
    
OpenSSLContext.java | 28.2 KB |

/* * 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.tomcat.util.net.openssl; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.InputStreamReader; import java.lang.ref.Cleaner; import java.lang.ref.Cleaner.Cleanable; import java.nio.charset.StandardCharsets; import java.security.KeyManagementException; import java.security.PrivateKey; import java.security.SecureRandom; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.Iterator; import java.util.List; import java.util.concurrent.locks.Lock; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLSessionContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509KeyManager; import javax.net.ssl.X509TrustManager; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.jni.AprStatus; import org.apache.tomcat.jni.Pool; import org.apache.tomcat.jni.SSL; import org.apache.tomcat.jni.SSLConf; import org.apache.tomcat.jni.SSLContext; import org.apache.tomcat.util.net.Constants; import org.apache.tomcat.util.net.SSLHostConfig; import org.apache.tomcat.util.net.SSLHostConfig.CertificateVerification; import org.apache.tomcat.util.net.SSLHostConfigCertificate; import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; import org.apache.tomcat.util.net.SSLUtilBase; import org.apache.tomcat.util.res.StringManager; public class OpenSSLContext implements org.apache.tomcat.util.net.SSLContext { private static final Log log = LogFactory.getLog(OpenSSLContext.class); private static final StringManager sm = StringManager.getManager(OpenSSLContext.class); private static final String defaultProtocol = "TLS"; private static final String BEGIN_KEY = "-----BEGIN PRIVATE KEY----- "; private static final Object END_KEY = " -----END PRIVATE KEY-----"; static final CertificateFactory X509_CERT_FACTORY; static { try { X509_CERT_FACTORY = CertificateFactory.getInstance("X.509"); } catch (CertificateException e) { throw new IllegalStateException(sm.getString("openssl.X509FactoryError"), e); } } private static final Cleaner cleaner = Cleaner.create(); private final SSLHostConfig sslHostConfig; private final SSLHostConfigCertificate certificate; private final List<String> negotiableProtocols; private OpenSSLSessionContext sessionContext; private X509TrustManager x509TrustManager; private String enabledProtocol; private boolean initialized = false; private final OpenSSLState state; private final Cleanable cleanable; public OpenSSLContext(SSLHostConfigCertificate certificate, List<String> negotiableProtocols) throws SSLException { this.sslHostConfig = certificate.getSSLHostConfig(); this.certificate = certificate; long aprPool = Pool.create(0); long cctx = 0; long ctx = 0; boolean success = false; try { // Create OpenSSLConfCmd context if used if (sslHostConfig.getOpenSslConf() == null && sslHostConfig.getTrustManagerClassName() == null && sslHostConfig.getTruststore() == null) { /* * If an instance of OpenSSLConf is required, it must be created here so the reference can be placed in * the (immutable) OpenSSLState record. * * If OpenSSL managed trust is used, an instance of OpenSSLConf is required to pass OCSP configuration * parameters to Tomcat Native. Create one if one hasn't already been created. */ sslHostConfig.setOpenSslConf(new OpenSSLConf()); } if (sslHostConfig.getOpenSslConf() != null) { try { if (log.isTraceEnabled()) { log.trace(sm.getString("openssl.makeConf")); } cctx = SSLConf.make(aprPool, SSL.SSL_CONF_FLAG_FILE | SSL.SSL_CONF_FLAG_SERVER | SSL.SSL_CONF_FLAG_CERTIFICATE | SSL.SSL_CONF_FLAG_SHOW_ERRORS); } catch (Exception e) { throw new SSLException(sm.getString("openssl.errMakeConf"), e); } } sslHostConfig.setOpenSslConfContext(Long.valueOf(cctx)); // SSL protocol int value = SSL.SSL_PROTOCOL_NONE; for (String protocol : sslHostConfig.getEnabledProtocols()) { if (Constants.SSL_PROTO_SSLv2Hello.equalsIgnoreCase(protocol)) { // NO-OP. OpenSSL always supports SSLv2Hello } else if (Constants.SSL_PROTO_SSLv2.equalsIgnoreCase(protocol)) { value |= SSL.SSL_PROTOCOL_SSLV2; } else if (Constants.SSL_PROTO_SSLv3.equalsIgnoreCase(protocol)) { value |= SSL.SSL_PROTOCOL_SSLV3; } else if (Constants.SSL_PROTO_TLSv1.equalsIgnoreCase(protocol)) { value |= SSL.SSL_PROTOCOL_TLSV1; } else if (Constants.SSL_PROTO_TLSv1_1.equalsIgnoreCase(protocol)) { value |= SSL.SSL_PROTOCOL_TLSV1_1; } else if (Constants.SSL_PROTO_TLSv1_2.equalsIgnoreCase(protocol)) { value |= SSL.SSL_PROTOCOL_TLSV1_2; } else if (Constants.SSL_PROTO_TLSv1_3.equalsIgnoreCase(protocol)) { value |= SSL.SSL_PROTOCOL_TLSV1_3; } else if (Constants.SSL_PROTO_ALL.equalsIgnoreCase(protocol)) { value |= SSL.SSL_PROTOCOL_ALL; } else { // Should not happen since filtering to build // enabled protocols removes invalid values. throw new Exception(sm.getString("openssl.invalidSslProtocol", protocol)); } } // Create SSL Context try { ctx = SSLContext.make(aprPool, value, SSL.SSL_MODE_SERVER); } catch (Exception e) { // If the sslEngine is disabled on the AprLifecycleListener // there will be an Exception here but there is no way to check // the AprLifecycleListener settings from here throw new Exception(sm.getString("openssl.failSslContextMake"), e); } this.negotiableProtocols = negotiableProtocols; success = true; } catch (Exception e) { throw new SSLException(sm.getString("openssl.errorSSLCtxInit"), e); } finally { state = new OpenSSLState(aprPool, cctx, ctx); /* * When an SSLHostConfig is replaced at runtime, it is not possible to call destroy() on the associated * OpenSSLContext since it is likely that there will be in-progress connections using the OpenSSLContext. A * reference chain has been deliberately established (see OpenSSLSessionContext) to ensure that the * OpenSSLContext remains ineligible for GC while those connections are alive. Once those connections * complete, the OpenSSLContext will become eligible for GC and this method will ensure that the associated * native resources are cleaned up. */ cleanable = cleaner.register(this, state); if (!success) { destroy(); } } } public String getEnabledProtocol() { return enabledProtocol; } public void setEnabledProtocol(String protocol) { enabledProtocol = (protocol == null) ? defaultProtocol : protocol; } @Override public void destroy() { cleanable.clean(); } protected static boolean checkConf(OpenSSLConf conf, long cctx) throws Exception { boolean result = true; OpenSSLConfCmd cmd; String name; String value; int rc; for (OpenSSLConfCmd command : conf.getCommands()) { cmd = command; name = cmd.getName(); value = cmd.getValue(); if (name == null) { log.error(sm.getString("opensslconf.noCommandName", value)); result = false; continue; } if (log.isTraceEnabled()) { log.trace(sm.getString("opensslconf.checkCommand", name, value)); } try { rc = SSLConf.check(cctx, name, value); } catch (Exception e) { log.error(sm.getString("opensslconf.checkFailed")); return false; } if (rc <= 0) { log.error(sm.getString("opensslconf.failedCommand", name, value, Integer.toString(rc))); result = false; } else if (log.isTraceEnabled()) { log.trace(sm.getString("opensslconf.resultCommand", name, value, Integer.toString(rc))); } } if (!result) { log.error(sm.getString("opensslconf.checkFailed")); } return result; } protected static boolean applyConf(OpenSSLConf conf, long cctx, long ctx) throws Exception { boolean result = true; SSLConf.assign(cctx, ctx); OpenSSLConfCmd cmd; String name; String value; int rc; for (OpenSSLConfCmd command : conf.getCommands()) { cmd = command; name = cmd.getName(); value = cmd.getValue(); if (name == null) { log.error(sm.getString("opensslconf.noCommandName", value)); result = false; continue; } if (log.isTraceEnabled()) { log.trace(sm.getString("opensslconf.applyCommand", name, value)); } try { rc = SSLConf.apply(cctx, name, value); } catch (Exception e) { log.error(sm.getString("opensslconf.applyFailed")); return false; } if (rc <= 0) { log.error(sm.getString("opensslconf.failedCommand", name, value, Integer.toString(rc))); result = false; } else if (log.isTraceEnabled()) { log.trace(sm.getString("opensslconf.resultCommand", name, value, Integer.toString(rc))); } } rc = SSLConf.finish(cctx); if (rc <= 0) { log.error(sm.getString("opensslconf.finishFailed", Integer.toString(rc))); result = false; } if (!result) { log.error(sm.getString("opensslconf.applyFailed")); } return result; } /** * Setup the SSL_CTX. * * @param kms Must contain a KeyManager of the type {@code OpenSSLKeyManager} * @param tms Must contain a TrustManager of the type {@code X509TrustManager} * @param sr Is not used for this implementation. * * @throws KeyManagementException if an error occurs */ @Override public void init(KeyManager[] kms, TrustManager[] tms, SecureRandom sr) throws KeyManagementException { if (initialized) { log.warn(sm.getString("openssl.doubleInit")); return; } try { if (sslHostConfig.getInsecureRenegotiation()) { SSLContext.setOptions(state.ctx, SSL.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION); } else { SSLContext.clearOptions(state.ctx, SSL.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION); } // Use server's preference order for ciphers (rather than // client's) if (sslHostConfig.getHonorCipherOrder()) { SSLContext.setOptions(state.ctx, SSL.SSL_OP_CIPHER_SERVER_PREFERENCE); } else { SSLContext.clearOptions(state.ctx, SSL.SSL_OP_CIPHER_SERVER_PREFERENCE); } // Disable compression if requested if (sslHostConfig.getDisableCompression()) { SSLContext.setOptions(state.ctx, SSL.SSL_OP_NO_COMPRESSION); } else { SSLContext.clearOptions(state.ctx, SSL.SSL_OP_NO_COMPRESSION); } // Disable TLS Session Tickets (RFC4507) to protect perfect forward secrecy if (sslHostConfig.getDisableSessionTickets()) { SSLContext.setOptions(state.ctx, SSL.SSL_OP_NO_TICKET); } else { SSLContext.clearOptions(state.ctx, SSL.SSL_OP_NO_TICKET); } // Configure the ciphers that the client is permitted to negotiate SSLContext.setCipherSuite(state.ctx, sslHostConfig.getCiphers()); SSLContext.setCipherSuitesEx(state.ctx, sslHostConfig.getCipherSuites()); // If there is no certificate file must be using a KeyStore so a KeyManager is required. // If there is a certificate file a KeyManager is helpful but not strictly necessary. certificate.setCertificateKeyManager( OpenSSLUtil.chooseKeyManager(kms, certificate.getCertificateFile() == null)); addCertificate(certificate); // Client certificate verification int value = switch (sslHostConfig.getCertificateVerification()) { case NONE -> SSL.SSL_CVERIFY_NONE; case OPTIONAL -> SSL.SSL_CVERIFY_OPTIONAL; case OPTIONAL_NO_CA -> SSL.SSL_CVERIFY_OPTIONAL_NO_CA; case REQUIRED -> SSL.SSL_CVERIFY_REQUIRE; }; SSLContext.setVerify(state.ctx, value, sslHostConfig.getCertificateVerificationDepth()); if (tms != null) { // Client certificate verification based on custom trust managers x509TrustManager = chooseTrustManager(tms); SSLContext.setCertVerifyCallback(state.ctx, new OpenSSLCertificateVerifier(x509TrustManager)); // Pass along the DER encoded certificates of the accepted client // certificate issuers, so that their subjects can be presented // by the server during the handshake to allow the client choosing // an acceptable certificate for (X509Certificate caCert : x509TrustManager.getAcceptedIssuers()) { SSLContext.addClientCACertificateRaw(state.ctx, caCert.getEncoded()); if (log.isDebugEnabled()) { log.debug(sm.getString("openssl.addedClientCaCert", caCert.toString())); } } } else { // Client certificate verification based on trusted CA files and dirs SSLContext.setCACertificate(state.ctx, SSLHostConfig.adjustRelativePath(sslHostConfig.getCaCertificateFile()), SSLHostConfig.adjustRelativePath(sslHostConfig.getCaCertificatePath())); sslHostConfig.getOpenSslConf().addCmd(new OpenSSLConfCmd(OpenSSLConfCmd.NO_OCSP_CHECK, Boolean.toString(!sslHostConfig.getOcspEnabled()))); sslHostConfig.getOpenSslConf().addCmd(new OpenSSLConfCmd(OpenSSLConfCmd.OCSP_SOFT_FAIL, Boolean.toString(sslHostConfig.getOcspSoftFail()))); sslHostConfig.getOpenSslConf().addCmd(new OpenSSLConfCmd(OpenSSLConfCmd.OCSP_TIMEOUT, Integer.toString(sslHostConfig.getOcspTimeout()))); sslHostConfig.getOpenSslConf().addCmd(new OpenSSLConfCmd(OpenSSLConfCmd.OCSP_VERIFY_FLAGS, Integer.toString(sslHostConfig.getOcspVerifyFlags()))); } if (negotiableProtocols != null && !negotiableProtocols.isEmpty()) { List<String> protocols = new ArrayList<>(negotiableProtocols); protocols.add("http/1.1"); String[] protocolsArray = protocols.toArray(new String[0]); SSLContext.setAlpnProtos(state.ctx, protocolsArray, SSL.SSL_SELECTOR_FAILURE_NO_ADVERTISE); } // Apply OpenSSLConfCmd if used OpenSSLConf openSslConf = sslHostConfig.getOpenSslConf(); if (openSslConf != null && state.cctx != 0) { // Check OpenSSLConfCmd if used if (log.isTraceEnabled()) { log.trace(sm.getString("openssl.checkConf")); } try { if (!checkConf(openSslConf, state.cctx)) { log.error(sm.getString("openssl.errCheckConf")); throw new Exception(sm.getString("openssl.errCheckConf")); } } catch (Exception e) { throw new Exception(sm.getString("openssl.errCheckConf"), e); } if (log.isTraceEnabled()) { log.trace(sm.getString("openssl.applyConf")); } try { if (!applyConf(openSslConf, state.cctx, state.ctx)) { log.error(sm.getString("openssl.errApplyConf")); throw new SSLException(sm.getString("openssl.errApplyConf")); } } catch (Exception e) { throw new SSLException(sm.getString("openssl.errApplyConf"), e); } // Reconfigure the enabled protocols int opts = SSLContext.getOptions(state.ctx); List<String> enabled = new ArrayList<>(); // Seems like there is no way to explicitly disable SSLv2Hello // in OpenSSL so it is always enabled enabled.add(Constants.SSL_PROTO_SSLv2Hello); if ((opts & SSL.SSL_OP_NO_TLSv1) == 0) { enabled.add(Constants.SSL_PROTO_TLSv1); } if ((opts & SSL.SSL_OP_NO_TLSv1_1) == 0) { enabled.add(Constants.SSL_PROTO_TLSv1_1); } if ((opts & SSL.SSL_OP_NO_TLSv1_2) == 0) { enabled.add(Constants.SSL_PROTO_TLSv1_2); } if ((opts & SSL.SSL_OP_NO_SSLv2) == 0) { enabled.add(Constants.SSL_PROTO_SSLv2); } if ((opts & SSL.SSL_OP_NO_SSLv3) == 0) { enabled.add(Constants.SSL_PROTO_SSLv3); } sslHostConfig.setEnabledProtocols(enabled.toArray(new String[0])); // Reconfigure the enabled ciphers sslHostConfig.setEnabledCiphers(SSLContext.getCiphers(state.ctx)); } sessionContext = new OpenSSLSessionContext(this); // If client authentication is being used, OpenSSL requires that // this is set so always set it in case an app is configured to // require it sessionContext.setSessionIdContext(SSLContext.DEFAULT_SESSION_ID_CONTEXT); sslHostConfig.setOpenSslContext(Long.valueOf(state.ctx)); initialized = true; } catch (Exception e) { destroy(); throw new KeyManagementException(sm.getString("openssl.errorSSLCtxInit"), e); } } public void addCertificate(SSLHostConfigCertificate certificate) throws Exception { // Load Server key and certificate if (certificate.getCertificateFile() != null) { // Set certificate String passwordToUse; if (certificate.getCertificateKeyPasswordFile() != null) { try (BufferedReader reader = new BufferedReader(new InputStreamReader( new FileInputStream( SSLHostConfig.adjustRelativePath(certificate.getCertificateKeyPasswordFile())), StandardCharsets.UTF_8))) { passwordToUse = reader.readLine(); } } else { passwordToUse = certificate.getCertificateKeyPassword(); } SSLContext.setCertificate(state.ctx, SSLHostConfig.adjustRelativePath(certificate.getCertificateFile()), SSLHostConfig.adjustRelativePath(certificate.getCertificateKeyFile()), passwordToUse, getCertificateIndex(certificate)); // Set certificate chain file SSLContext.setCertificateChainFile(state.ctx, SSLHostConfig.adjustRelativePath(certificate.getCertificateChainFile()), false); // Set revocation SSLContext.setCARevocation(state.ctx, SSLHostConfig.adjustRelativePath(sslHostConfig.getCertificateRevocationListFile()), SSLHostConfig.adjustRelativePath(sslHostConfig.getCertificateRevocationListPath())); } else { String alias = certificate.getCertificateKeyAlias(); X509KeyManager x509KeyManager = certificate.getCertificateKeyManager(); if (alias == null) { alias = SSLUtilBase.DEFAULT_KEY_ALIAS; } X509Certificate[] chain = x509KeyManager.getCertificateChain(alias); if (chain == null) { alias = findAlias(x509KeyManager, certificate); chain = x509KeyManager.getCertificateChain(alias); } PrivateKey key = x509KeyManager.getPrivateKey(alias); String encodedKey = BEGIN_KEY + Base64.getMimeEncoder(64, new byte[] { ' ' }).encodeToString(key.getEncoded()) + END_KEY; SSLContext.setCertificateRaw(state.ctx, chain[0].getEncoded(), encodedKey.getBytes(StandardCharsets.US_ASCII), getCertificateIndex(certificate)); for (int i = 1; i < chain.length; i++) { SSLContext.addChainCertificateRaw(state.ctx, chain[i].getEncoded()); } } } private static int getCertificateIndex(SSLHostConfigCertificate certificate) { int result; // If the type is undefined there will only be one certificate (enforced // in SSLHostConfig) so use the RSA slot. if (certificate.getType() == Type.RSA || certificate.getType() == Type.UNDEFINED) { result = SSL.SSL_AIDX_RSA; } else if (certificate.getType() == Type.EC) { result = SSL.SSL_AIDX_ECC; } else if (certificate.getType() == Type.DSA || certificate.getType() == Type.MLDSA) { result = SSL.SSL_AIDX_DSA; } else { result = SSL.SSL_AIDX_MAX; } return result; } /* * Find a valid alias when none was specified in the config. */ private static String findAlias(X509KeyManager keyManager, SSLHostConfigCertificate certificate) { Type type = certificate.getType(); String result = null; List<Type> candidateTypes = new ArrayList<>(); if (Type.UNDEFINED.equals(type)) { // Try all types to find an suitable alias candidateTypes.addAll(Arrays.asList(Type.values())); candidateTypes.remove(Type.UNDEFINED); } else { // Look for the specific type to find a suitable alias candidateTypes.add(type); } Iterator<Type> iter = candidateTypes.iterator(); while (result == null && iter.hasNext()) { result = keyManager.chooseServerAlias(iter.next().getKeyType(), null, null); } return result; } private static X509TrustManager chooseTrustManager(TrustManager[] managers) { for (TrustManager m : managers) { if (m instanceof X509TrustManager) { return (X509TrustManager) m; } } throw new IllegalStateException(sm.getString("openssl.trustManagerMissing")); } long getSSLContextID() { return state.ctx; } @Override public SSLSessionContext getServerSessionContext() { return sessionContext; } @Override public SSLEngine createSSLEngine() { return new OpenSSLEngine(cleaner, state.ctx, defaultProtocol, false, sessionContext, (negotiableProtocols != null && !negotiableProtocols.isEmpty()), initialized, sslHostConfig.getCertificateVerificationDepth(), sslHostConfig.getCertificateVerification() == CertificateVerification.OPTIONAL_NO_CA); } @Override public SSLServerSocketFactory getServerSocketFactory() { throw new UnsupportedOperationException(); } @Override public SSLParameters getSupportedSSLParameters() { throw new UnsupportedOperationException(); } @Override public X509Certificate[] getCertificateChain(String alias) { X509Certificate[] chain = null; X509KeyManager x509KeyManager = certificate.getCertificateKeyManager(); if (x509KeyManager != null) { if (alias == null) { alias = SSLUtilBase.DEFAULT_KEY_ALIAS; } chain = x509KeyManager.getCertificateChain(alias); if (chain == null) { alias = findAlias(x509KeyManager, certificate); chain = x509KeyManager.getCertificateChain(alias); } } return chain; } @Override public X509Certificate[] getAcceptedIssuers() { X509Certificate[] acceptedCerts = null; if (x509TrustManager != null) { acceptedCerts = x509TrustManager.getAcceptedIssuers(); } return acceptedCerts; } /** * @param aprPool the APR pool * @param cctx OpenSSLConfCmd context * @param ctx SSL context */ private record OpenSSLState(long aprPool, long cctx, long ctx) implements Runnable { @Override public void run() { /* * During shutdown there is a possibility that both the cleaner and the APR library termination code try and * free these resources. If both call free, there will be a JVM crash. * * If the cleaner frees the resources, the APR library termination won't try free them as well. * * If the APR library termination frees the resources, the cleaner MUST NOT attempt to do so. * * The locks and checks below ensure that a) the cleaner only runs if the APR library has not yet been * terminated and that the APR library status will not change while the cleaner is running. */ Lock readLock = AprStatus.getStatusLock().readLock(); readLock.lock(); try { if (AprStatus.isAprInitialized()) { if (ctx != 0) { SSLContext.free(ctx); } if (cctx != 0) { SSLConf.free(cctx); } if (aprPool != 0) { Pool.destroy(aprPool); } } } finally { readLock.unlock(); } } } }
Detected license expression
apache-2.0
Detected license expression (SPDX)
Apache-2.0
Percentage of license text
4.64
Copyrights

      
    
Holders

      
    
Authors

      
    
License detections License expression License expression SPDX
apache_2_0-4bde3f57-78aa-4201-96bf-531cba09e7de apache-2.0 Apache-2.0
URL Start line End line
http://www.apache.org/licenses/LICENSE-2.0 9 9