/*_############################################################################
  _##
  _##  SNMP4J-Agent 3 - SnmpTlsTmMibTest.java
  _##
  _##  Copyright (C) 2005-2025  Frank Fock (SNMP4J.org)
  _##
  _##  Licensed 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.snmp4j.agent.mo.snmp;

import org.junit.Before;
import org.junit.Test;
import org.snmp4j.CommandResponderEvent;
import org.snmp4j.MessageDispatcherImpl;
import org.snmp4j.PDU;
import org.snmp4j.agent.DefaultMOServer;
import org.snmp4j.agent.ManagedObject;
import org.snmp4j.agent.MOServer;
import org.snmp4j.agent.mo.DefaultMOFactory;
import org.snmp4j.agent.mo.MOColumn;
import org.snmp4j.agent.mo.MOMutableColumn;
import org.snmp4j.agent.mo.MOTable;
import org.snmp4j.agent.request.Request;
import org.snmp4j.agent.request.SnmpRequest;
import org.snmp4j.mp.MPv3;
import org.snmp4j.mp.PduHandle;
import org.snmp4j.security.SecurityLevel;
import org.snmp4j.security.SecurityModel;
import org.snmp4j.smi.Integer32;
import org.snmp4j.smi.OID;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.UdpAddress;
import org.snmp4j.smi.Variable;
import org.snmp4j.smi.VariableBinding;
import org.snmp4j.transport.tls.TLSTMUtil;

import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Iterator;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

/**
 * Tests the conformance of {@link SnmpTlsTmMib} with RFC 6353.
 *
 * @author Frank Fock
 */
public class SnmpTlsTmMibTest {

    /**
     * Self signed test certificate with the subject {@code CN="Täst, Agent", O=SNMP4J, C=DE} and the
     * subjectAltName values {@code rfc822Name=FooBar@Example.COM}, {@code dNSName=Host.Example.COM}, and
     * {@code iPAddress=192.0.2.1} - in that order.
     */
    private static final String CERT_A =
            "MIIDRDCCAiygAwIBAgIICSGrvICHAgkwDQYJKoZIhvcNAQELBQAwNTELMAkGA1UEBhMCREUxDzANBgNVBAoTBlNOTVA0SjEVMBMGA1" +
            "UEAwwMVMOkc3QsIEFnZW50MB4XDTI2MDcyODE0NDEzNFoXDTQ2MDcyMzE0NDEzNFowNTELMAkGA1UEBhMCREUxDzANBgNVBAoTBlNO" +
            "TVA0SjEVMBMGA1UEAwwMVMOkc3QsIEFnZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmVhZHT44Ac1DBRfqKumbP8r" +
            "o6ciUCMVRHwl78RYj0gE2b7GOGkKkZfo4MenRX644/v5YPXi8tGfIWiCyVfaU4TK/1dgDi47uMG9ZfoUs1yeW0qEJnUyEbEoj4m47dQ" +
            "dJs3zQ5RiiXEIPPU89QfUEB95nDJREXqHK2skEXSeFOIsNTLPu6zO/MqFt77I73msnJZO40TQH2dxZKZkFh1RJsu1DjutagkCD3/l20" +
            "ioibI9PSkctsH4jtId+V82pUfP9XHrir6XpvrVmulGZvkYfBrqZre6JiJiLWYdZ8CU2z5vHqNZJDYmsKBNtWEKx5Hr6xnkHDBtGzGDz" +
            "16hnmgMMFQIDAQABo1gwVjAdBgNVHQ4EFgQUOU5voAJ0ic/EOT5r5GRzuzo0qrcwNQYDVR0RBC4wLIESRm9vQmFyQEV4YW1wbGUuQ09" +
            "NghBIb3N0LkV4YW1wbGUuQ09NhwTAAAIBMA0GCSqGSIb3DQEBCwUAA4IBAQAOrmxWBhfOaJpPgZ/wztMJOdmB72xdtqD/fhMgmnI7Kn" +
            "BF+hRGPRkvJ7C2+2gpxI94iZlXbqyAjqybQgU8OZQtqzWSi/bCpCeKuYUpTiw0/MaJeTFJ1z8ueZcnypbeg50FZGRoo+CMNsFCrkRLA" +
            "w+dSSOp8Hqf3u+OqwzvG/XiUnERfDjecM0o95+U8WUQdcxXOJqCXw6W+4rsUqAcr5x+6UNtxR6w3V02dvMtP+vdFDZwXrPqI7lHq322" +
            "bO+78Bqr9fWJEpdx7uQoaMSjIIY0UJMrwYsR/8SuHPk7Q0kmE28CfauKfYZ5KOfbBfnCVhsJqhVrTn5FgHZE21oCAUE+";

    /**
     * Self signed test certificate with the subject {@code CN=host-b, O=SNMP4J} and the subjectAltName values
     * {@code dNSName=Other.Example.NET}, {@code rfc822Name=User@Example.ORG}, and {@code iPAddress=2001:db8::1} -
     * in that order.
     */
    private static final String CERT_B =
            "MIIDKjCCAhKgAwIBAgIJAIiSDq6JCiC7MA0GCSqGSIb3DQEBCwUAMCIxDzANBgNVBAoTBlNOTVA0SjEPMA0GA1UEAxMGaG9zdC1iMB4" +
            "XDTI2MDcyODE0NDEzNFoXDTQ2MDcyMzE0NDEzNFowIjEPMA0GA1UEChMGU05NUDRKMQ8wDQYDVQQDEwZob3N0LWIwggEiMA0GCSqGSI" +
            "b3DQEBAQUAA4IBDwAwggEKAoIBAQCrjDwGvITfG5yh7TPG28xCoQBEUjd6P6JwY3w48cfIdSpMTr3nQDWgif5TRQmCxF04leJKxiV/L" +
            "traC3CNGOTokZW/QXTDThBQf2USrjypQuDX1Bs04tO5NO5Uah7OHGvlkQAqEuNoBarpDbU/VqX2+e67T9gYXOVYYJOpd9HQyNmXPAzY" +
            "BmgZYKD/M5sadMkh3cSi1vyat8HVBKHcQlDD5A9ImBSIC5QTYd4LIJcNkfyKywSsiXWLWITfY1A1nFnyXp89sZLEJZuhaphyHfIs/ys" +
            "92QVbmxJyAqLqJVW3xHM2RSaSfT3QeLt7mc9s/cxB/zhCVHnrHRLp5Rv+q0v9AgMBAAGjYzBhMB0GA1UdDgQWBBSQLiTKCDWSs++YRL" +
            "Zit4iA1AZz+DBABgNVHREEOTA3ghFPdGhlci5FeGFtcGxlLk5FVIEQVXNlckBFeGFtcGxlLk9SR4cQIAENuAAAAAAAAAAAAAAAATANB" +
            "gkqhkiG9w0BAQsFAAOCAQEAiVoJktGgUMIZ2tUsboSONktHyxzYOQ6MlqmkJO86EWo7Ho+pPe4V8jRBHE6HBYMdvyL4YYcPDxSz3qua" +
            "IuFPGr0uhrXDqI5BBXR0Tkx9lR3kOxsi8/szt4e09B7Dt3rUqNqxIyFLOKBs72HyWJ8wihf3raal2Oay4SoNd9Cv4NINlPe6cCfCaWF" +
            "T4tMMzRg/hXtYdcWM+BP7YcVSBwTpk+a+yQ+b0Y/u7pS3cZ8DqF31wnUagkeqouUEQJ/JhI95MgEJ9KK4J55Cgh97o8svlK7YdpOpMR" +
            "YnmzWBS/y6cj9SyZydJv9MiNR/AyVPjr+RMelBbdKG4G+PRpFJF9BzMA==";

    /**
     * The hash algorithm identifier of SHA-256 in the IANA TLS HashAlgorithm registry (RFC 5246).
     */
    private static final int HASH_ALGORITHM_SHA256 = 4;

    private MessageDispatcherImpl messageDispatcher;
    private SnmpTlsTmMib snmpTlsTmMib;
    private X509Certificate certA;
    private X509Certificate certB;

    @Before
    public void setUp() throws Exception {
        messageDispatcher = new MessageDispatcherImpl();
        messageDispatcher.addMessageProcessingModel(new MPv3());
        certA = parseCertificate(CERT_A);
        certB = parseCertificate(CERT_B);
        snmpTlsTmMib = new SnmpTlsTmMib(DefaultMOFactory.getInstance());
        // The sysUpTime of the SNMPv2-MIB is needed by the *TableLastChanged objects:
        MOServer server = new DefaultMOServer();
        SNMPv2MIB snmpv2MIB = new SNMPv2MIB(new OctetString("SNMP4J-Agent test"),
                new OID("1.3.6.1.4.1.4976"), new Integer32(72));
        snmpv2MIB.registerMOs(server, null);
        snmpTlsTmMib.registerMOs(server, null);
    }

    private static X509Certificate parseCertificate(String base64DER) throws Exception {
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        return (X509Certificate) certificateFactory.generateCertificate(
                new ByteArrayInputStream(Base64.getDecoder().decode(base64DER)));
    }

    /**
     * Returns the RFC 6353 conformant {@code SnmpTLSFingerprint} value of the given certificate, i.e. the one octet
     * hash algorithm identifier followed by the hash value.
     */
    private static OctetString fingerprint(X509Certificate cert, int hashAlgorithmID, String algorithm)
            throws Exception {
        MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
        OctetString fingerprint = new OctetString(new byte[] { (byte) hashAlgorithmID });
        fingerprint.append(messageDigest.digest(cert.getEncoded()));
        return fingerprint;
    }

    private OID addCertToTSNRow(int id, OctetString fingerprint, OID mapType, OctetString data) {
        OID index = new OID(new int[] { id });
        MOTable<SnmpTlsTmMib.SnmpTlstmCertToTSNEntryRow, ? extends MOColumn, ?> table =
                snmpTlsTmMib.getSnmpTlstmCertToTSNEntry();
        table.addRow(table.createRow(index, new Variable[] {
                fingerprint, mapType, data, new Integer32(StorageType.nonVolatile),
                new Integer32(RowStatus.active) }));
        return index;
    }

    /**
     * Executes a SET request on the given table and returns the resulting SNMP error status.
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    private int set(MOTable<?, ?, ?> table, VariableBinding... vbs) {
        PDU pdu = new PDU();
        pdu.setType(PDU.SET);
        for (VariableBinding vb : vbs) {
            pdu.add(vb);
        }
        CommandResponderEvent<UdpAddress> event =
                new CommandResponderEvent<>(messageDispatcher, null, new UdpAddress("127.0.0.1/4711"),
                        MPv3.ID, SecurityModel.SECURITY_MODEL_USM, new byte[0], SecurityLevel.AUTH_PRIV,
                        new PduHandle(1), pdu, 65535, null);
        SnmpRequest request = new SnmpRequest(event, null);
        // the phases below mirror the SET processing of org.snmp4j.agent.CommandProcessor:
        request.setPhase(Request.PHASE_2PC_PREPARE);
        for (Iterator<SnmpRequest.SnmpSubRequest> it = request.iterator();
             !request.isPhaseComplete() && it.hasNext(); ) {
            SnmpRequest.SnmpSubRequest subRequest = it.next();
            subRequest.setTargetMO((ManagedObject) table);
            ((ManagedObject) table).prepare(subRequest);
        }
        request.nextPhase();
        if (request.getPhase() == Request.PHASE_2PC_COMMIT) {
            for (Iterator<SnmpRequest.SnmpSubRequest> it = request.iterator();
                 !request.isPhaseComplete() && it.hasNext(); ) {
                ((ManagedObject) table).commit(it.next());
            }
            request.nextPhase();
        }
        if (request.getPhase() == Request.PHASE_2PC_UNDO) {
            for (Iterator<SnmpRequest.SnmpSubRequest> it = request.iterator(); it.hasNext(); ) {
                ((ManagedObject) table).undo(it.next());
            }
            request.nextPhase();
        }
        for (Iterator<SnmpRequest.SnmpSubRequest> it = request.iterator(); it.hasNext(); ) {
            ((ManagedObject) table).cleanup(it.next());
        }
        return request.getErrorStatus();
    }

    /**
     * Creates a {@link VariableBinding} for the instance of a columnar object. Note that {@code column} must not be
     * modified by this method, because the columnar OIDs of {@link SnmpTlsTmMib} are constants.
     */
    private static VariableBinding vb(OID entry, int column, OID index, Variable value) {
        OID oid = new OID(entry);
        oid.append(column);
        oid.append(index);
        return new VariableBinding(oid, value);
    }

    // Defect 1: columns that RFC 6353 does not allow to be modified while their row is active(1).

    @Test
    public void testColumnsNotModifiableWhileRowIsActive() {
        assertNotMutableInService(snmpTlsTmMib.getSnmpTlstmCertToTSNEntry(),
                SnmpTlsTmMib.idxSnmpTlstmCertToTSNFingerprint);
        assertNotMutableInService(snmpTlsTmMib.getSnmpTlstmCertToTSNEntry(),
                SnmpTlsTmMib.idxSnmpTlstmCertToTSNMapType);
        assertNotMutableInService(snmpTlsTmMib.getSnmpTlstmCertToTSNEntry(),
                SnmpTlsTmMib.idxSnmpTlstmCertToTSNData);
        assertNotMutableInService(snmpTlsTmMib.getSnmpTlstmParamsEntry(),
                SnmpTlsTmMib.idxSnmpTlstmParamsClientFingerprint);
        assertNotMutableInService(snmpTlsTmMib.getSnmpTlstmAddrEntry(),
                SnmpTlsTmMib.idxSnmpTlstmAddrServerFingerprint);
        // the storage type columns must remain modifiable while their row is active:
        assertTrue(((MOMutableColumn<?>) snmpTlsTmMib.getSnmpTlstmCertToTSNEntry().
                getColumn(SnmpTlsTmMib.idxSnmpTlstmCertToTSNStorageType)).isMutableInService());
    }

    private static void assertNotMutableInService(MOTable<?, ? extends MOColumn, ?> table, int columnIndex) {
        assertFalse(((MOMutableColumn<?>) table.getColumn(columnIndex)).isMutableInService());
    }

    @Test
    public void testSetFingerprintOfActiveRowReturnsInconsistentValue() throws Exception {
        OctetString fpA = fingerprint(certA, HASH_ALGORITHM_SHA256, "SHA-256");
        OctetString fpB = fingerprint(certB, HASH_ALGORITHM_SHA256, "SHA-256");
        MOTable<?, ?, ?> table = snmpTlsTmMib.getSnmpTlstmCertToTSNEntry();
        OID index = new OID(new int[] { 10 });
        assertEquals(PDU.noError, set(table,
                vb(SnmpTlsTmMib.oidSnmpTlstmCertToTSNEntry, SnmpTlsTmMib.colSnmpTlstmCertToTSNFingerprint, index, fpA),
                vb(SnmpTlsTmMib.oidSnmpTlstmCertToTSNEntry, SnmpTlsTmMib.colSnmpTlstmCertToTSNRowStatus, index,
                        new Integer32(RowStatus.createAndGo))));
        assertTrue(RowStatus.isRowActive(snmpTlsTmMib.getSnmpTlstmCertToTSNEntry().getModel().getRow(index),
                SnmpTlsTmMib.idxSnmpTlstmCertToTSNRowStatus));
        // now the fingerprint, map type, and data columns are frozen:
        assertEquals(PDU.inconsistentValue, set(table,
                vb(SnmpTlsTmMib.oidSnmpTlstmCertToTSNEntry, SnmpTlsTmMib.colSnmpTlstmCertToTSNFingerprint, index, fpB)));
        assertEquals(PDU.inconsistentValue, set(table,
                vb(SnmpTlsTmMib.oidSnmpTlstmCertToTSNEntry, SnmpTlsTmMib.colSnmpTlstmCertToTSNMapType, index,
                        SnmpTlsTmMib.oidSnmpTlstmCertSANDNSName)));
        assertEquals(PDU.inconsistentValue, set(table,
                vb(SnmpTlsTmMib.oidSnmpTlstmCertToTSNEntry, SnmpTlsTmMib.colSnmpTlstmCertToTSNData, index, new OctetString("other"))));
        // the fingerprint has not been changed:
        assertEquals(fpA, snmpTlsTmMib.getSnmpTlstmCertToTSNEntry().getModel().getRow(index).
                getSnmpTlstmCertToTSNFingerprint());
    }

    // Defect 2: SnmpTLSFingerprint values include a one octet hash algorithm identifier.

    @Test
    public void testFingerprintWithHashAlgorithmIdentifier() throws Exception {
        assertTrue(TLSTMUtil.isMatchingFingerprint(certA,
                fingerprint(certA, HASH_ALGORITHM_SHA256, "SHA-256")));
        assertTrue(TLSTMUtil.isMatchingFingerprint(certA, fingerprint(certA, 2, "SHA-1")));
        assertTrue(TLSTMUtil.isMatchingFingerprint(certA, fingerprint(certA, 6, "SHA-512")));
        assertFalse(TLSTMUtil.isMatchingFingerprint(certB,
                fingerprint(certA, HASH_ALGORITHM_SHA256, "SHA-256")));
        // a zero length fingerprint never matches:
        assertFalse(TLSTMUtil.isMatchingFingerprint(certA, new OctetString()));
        // fingerprints without hash algorithm identifier are still accepted for backwards compatibility:
        assertTrue(TLSTMUtil.isMatchingFingerprint(certA, TLSTMUtil.getFingerprint(certA)));
    }

    @Test
    public void testGetSecurityNameWithRFC6353Fingerprint() throws Exception {
        addCertToTSNRow(10, fingerprint(certA, HASH_ALGORITHM_SHA256, "SHA-256"),
                SnmpTlsTmMib.oidSnmpTlstmCertSANDNSName, new OctetString());
        assertEquals(new OctetString("host.example.com"),
                snmpTlsTmMib.getSecurityName(new X509Certificate[] { certA }));
        // the fingerprint of another certificate must not match:
        assertNull(snmpTlsTmMib.getSecurityName(new X509Certificate[] { certB }));
    }

    // Defect 3: snmpTlstmCertCommonName maps the CommonName only, not the whole distinguished name.

    @Test
    public void testCommonNameMapping() {
        assertEquals(new OctetString("Täst, Agent".getBytes(StandardCharsets.UTF_8)),
                SnmpTlsTmMib.getCommonName(certA.getSubjectX500Principal()));
        assertEquals(new OctetString("host-b"), SnmpTlsTmMib.getCommonName(certB.getSubjectX500Principal()));
        assertEquals(new OctetString("host-b"), SnmpTlsTmMib.mapCommonName(certB));
    }

    @Test
    public void testGetSecurityNameByCommonName() throws Exception {
        addCertToTSNRow(10, fingerprint(certB, HASH_ALGORITHM_SHA256, "SHA-256"),
                SnmpTlsTmMib.oidSnmpTlstmCertCommonName, new OctetString());
        assertEquals(new OctetString("host-b"), snmpTlsTmMib.getSecurityName(new X509Certificate[] { certB }));
    }

    // Defect 4: snmpTlstmCertSANAny uses the first matching subjectAltName found in the certificate.

    @Test
    public void testSubjAltNameAnyUsesCertificateOrder() throws Exception {
        // certificate A has the rfc822Name first:
        assertEquals(new OctetString("FooBar@example.com"), SnmpTlsTmMib.mapSubjAltNameAny(certA));
        // certificate B has the dNSName first:
        assertEquals(new OctetString("other.example.net"), SnmpTlsTmMib.mapSubjAltNameAny(certB));
    }

    @Test
    public void testSingleSubjAltNameMappings() throws Exception {
        assertEquals(new OctetString("FooBar@example.com"), SnmpTlsTmMib.mapRFC822Name("FooBar@Example.COM"));
        assertEquals(new OctetString("host.example.com"), SnmpTlsTmMib.mapDNSName("Host.Example.COM"));
        assertEquals(new OctetString("192.0.2.1"),
                TLSTMUtil.getIpAddressFromSubjAltName(certA.getSubjectAlternativeNames()));
    }

    // Defect 5: the *TableLastChanged objects report the time of the last modification of their table.

    @Test
    public void testTableLastChangedIsUpdatedOnTableModification() throws Exception {
        assertEquals(0, snmpTlsTmMib.getSnmpTlstmCertToTSNTableLastChanged().getValue().toLong());
        assertTrue(snmpTlsTmMib.getSnmpTlstmCertToTSNTableLastChanged().isVolatile());
        // make sure that sysUpTime has been increased at least by one time tick:
        Thread.sleep(50);
        OID index = addCertToTSNRow(10, fingerprint(certA, HASH_ALGORITHM_SHA256, "SHA-256"),
                SnmpTlsTmMib.oidSnmpTlstmCertSpecified, new OctetString("user"));
        long afterAdd = snmpTlsTmMib.getSnmpTlstmCertToTSNTableLastChanged().getValue().toLong();
        assertTrue("snmpTlstmCertToTSNTableLastChanged was not updated on row creation", afterAdd > 0);
        // the other tables have not been modified:
        assertEquals(0, snmpTlsTmMib.getSnmpTlstmParamsTableLastChanged().getValue().toLong());
        assertEquals(0, snmpTlsTmMib.getSnmpTlstmAddrTableLastChanged().getValue().toLong());
        Thread.sleep(50);
        snmpTlsTmMib.getSnmpTlstmCertToTSNEntry().removeRow(index);
        assertTrue("snmpTlstmCertToTSNTableLastChanged was not updated on row deletion",
                snmpTlsTmMib.getSnmpTlstmCertToTSNTableLastChanged().getValue().toLong() > afterAdd);
    }

    // Defect 6: rows of the snmpTlstmAddrTable must not be active with a blank fingerprint and a wildcard identity.

    @Test
    public void testAddrRowWithBlankFingerprintAndWildcardIdentityCannotBeActivatedByCreateAndGo() {
        MOTable<?, ?, ?> table = snmpTlsTmMib.getSnmpTlstmAddrEntry();
        OID index = new OctetString("target1").toSubIndex(true);
        assertEquals(PDU.inconsistentValue, set(table,
                vb(SnmpTlsTmMib.oidSnmpTlstmAddrEntry, SnmpTlsTmMib.colSnmpTlstmAddrServerIdentity, index, new OctetString("*")),
                vb(SnmpTlsTmMib.oidSnmpTlstmAddrEntry, SnmpTlsTmMib.colSnmpTlstmAddrRowStatus, index,
                        new Integer32(RowStatus.createAndGo))));
        assertNull(snmpTlsTmMib.getSnmpTlstmAddrEntry().getModel().getRow(index));
    }

    @Test
    public void testAddrRowWithBlankFingerprintAndWildcardIdentityCannotBeActivatedByRowStatus() {
        MOTable<?, ?, ?> table = snmpTlsTmMib.getSnmpTlstmAddrEntry();
        OID index = new OctetString("target2").toSubIndex(true);
        assertEquals(PDU.noError, set(table,
                vb(SnmpTlsTmMib.oidSnmpTlstmAddrEntry, SnmpTlsTmMib.colSnmpTlstmAddrRowStatus, index,
                        new Integer32(RowStatus.createAndWait))));
        assertEquals(PDU.noError, set(table,
                vb(SnmpTlsTmMib.oidSnmpTlstmAddrEntry, SnmpTlsTmMib.colSnmpTlstmAddrServerIdentity, index, new OctetString("*"))));
        // activating the row by setting its row status alone must be rejected too:
        assertEquals(PDU.inconsistentValue, set(table,
                vb(SnmpTlsTmMib.oidSnmpTlstmAddrEntry, SnmpTlsTmMib.colSnmpTlstmAddrRowStatus, index, new Integer32(RowStatus.active))));
        assertFalse(RowStatus.isRowActive(snmpTlsTmMib.getSnmpTlstmAddrEntry().getModel().getRow(index),
                SnmpTlsTmMib.idxSnmpTlstmAddrRowStatus));
    }

    @Test
    public void testAddrRowWithFingerprintCanBeActivated() throws Exception {
        MOTable<?, ?, ?> table = snmpTlsTmMib.getSnmpTlstmAddrEntry();
        OID index = new OctetString("target3").toSubIndex(true);
        assertEquals(PDU.noError, set(table,
                vb(SnmpTlsTmMib.oidSnmpTlstmAddrEntry, SnmpTlsTmMib.colSnmpTlstmAddrServerFingerprint, index,
                        fingerprint(certA, HASH_ALGORITHM_SHA256, "SHA-256")),
                vb(SnmpTlsTmMib.oidSnmpTlstmAddrEntry, SnmpTlsTmMib.colSnmpTlstmAddrServerIdentity, index, new OctetString("*")),
                vb(SnmpTlsTmMib.oidSnmpTlstmAddrEntry, SnmpTlsTmMib.colSnmpTlstmAddrRowStatus, index,
                        new Integer32(RowStatus.createAndGo))));
        assertTrue(RowStatus.isRowActive(snmpTlsTmMib.getSnmpTlstmAddrEntry().getModel().getRow(index),
                SnmpTlsTmMib.idxSnmpTlstmAddrRowStatus));
    }

    // Defect 7: a client certificate that does not match any fingerprint is left to path validation.

    @Test
    public void testClientCertificateIsNotRejectedIfNoFingerprintMatches() throws Exception {
        addCertToTSNRow(10, fingerprint(certB, HASH_ALGORITHM_SHA256, "SHA-256"),
                SnmpTlsTmMib.oidSnmpTlstmCertSANDNSName, new OctetString());
        // certificate A does not match, but it must not be rejected because it could be validated by
        // certification path validation:
        assertFalse(snmpTlsTmMib.isClientCertificateAccepted(certA));
        assertTrue(snmpTlsTmMib.isClientCertificateAccepted(certB));
    }

    // Defect 8: snmpTlstmCertSpecified requires a non-zero length snmpTlstmCertToTSNData value.

    @Test
    public void testCertSpecifiedWithoutDataFails() throws Exception {
        addCertToTSNRow(10, fingerprint(certA, HASH_ALGORITHM_SHA256, "SHA-256"),
                SnmpTlsTmMib.oidSnmpTlstmCertSpecified, new OctetString());
        assertNull(snmpTlsTmMib.getSecurityName(new X509Certificate[] { certA }));
        // a row with a non-zero length data value maps successfully:
        addCertToTSNRow(20, fingerprint(certA, HASH_ALGORITHM_SHA256, "SHA-256"),
                SnmpTlsTmMib.oidSnmpTlstmCertSpecified, new OctetString("myUser"));
        assertEquals(new OctetString("myUser"), snmpTlsTmMib.getSecurityName(new X509Certificate[] { certA }));
    }

    // Defect 9: malformed rfc822Name values are ignored instead of causing an exception.

    @Test
    public void testMalformedRFC822NameIsIgnored() {
        assertNull(SnmpTlsTmMib.mapRFC822Name("no-at-sign"));
        assertNull(SnmpTlsTmMib.mapRFC822Name("@example.com"));
        assertNull(SnmpTlsTmMib.mapRFC822Name("user@"));
        assertNull(SnmpTlsTmMib.mapRFC822Name(null));
        assertNull(SnmpTlsTmMib.mapDNSName(null));
    }

    // Defect 10: snmpTlstmCertToTSNID is an Unsigned32 (1..4294967295).

    @Test
    public void testCertToTSNIndexValidation() {
        MOTable<?, ?, ?> table = snmpTlsTmMib.getSnmpTlstmCertToTSNEntry();
        assertFalse(table.getIndexDef().isValidIndex(new OID(new int[] { 0 })));
        assertTrue(table.getIndexDef().isValidIndex(new OID(new int[] { 1 })));
        assertTrue(table.getIndexDef().isValidIndex(new OID(new int[] { 0xFFFFFFFF })));
        assertFalse(table.getIndexDef().isValidIndex(new OID(new int[] { 1, 2 })));
        assertFalse(table.getIndexDef().isValidIndex(new OID(new int[0])));
    }

    @Test
    public void testCertToTSNRowWithZeroIndexCannotBeCreated() throws Exception {
        MOTable<?, ?, ?> table = snmpTlsTmMib.getSnmpTlstmCertToTSNEntry();
        OID index = new OID(new int[] { 0 });
        assertTrue(PDU.noError != set(table,
                vb(SnmpTlsTmMib.oidSnmpTlstmCertToTSNEntry, SnmpTlsTmMib.colSnmpTlstmCertToTSNFingerprint, index,
                        fingerprint(certA, HASH_ALGORITHM_SHA256, "SHA-256")),
                vb(SnmpTlsTmMib.oidSnmpTlstmCertToTSNEntry, SnmpTlsTmMib.colSnmpTlstmCertToTSNRowStatus, index,
                        new Integer32(RowStatus.createAndGo))));
        assertNull(snmpTlsTmMib.getSnmpTlstmCertToTSNEntry().getModel().getRow(index));
    }

    // Defect 11: IPv6 subjectAltName values are mapped to a 32 character lower case hex string.

    @Test
    public void testIpAddressMapping() throws Exception {
        assertEquals(new OctetString("20010db8000000000000000000000001"),
                TLSTMUtil.formatIpAddressForTmSecurityName("2001:db8:0:0:0:0:0:1"));
        assertEquals(new OctetString("20010db8000000000000000000000001"),
                TLSTMUtil.formatIpAddressForTmSecurityName("2001:DB8::1"));
        assertEquals(new OctetString("00000000000000000000000000000000"),
                TLSTMUtil.formatIpAddressForTmSecurityName("::"));
        assertEquals(new OctetString("00000000000000000000ffffc0000201"),
                TLSTMUtil.formatIpAddressForTmSecurityName("::ffff:192.0.2.1"));
        assertEquals(new OctetString("fe800000000000000000000000000001"),
                TLSTMUtil.formatIpAddressForTmSecurityName("[fe80::1]%eth0"));
        assertEquals(new OctetString("192.0.2.1"), TLSTMUtil.formatIpAddressForTmSecurityName("192.0.2.1"));
        assertNull(TLSTMUtil.formatIpAddressForTmSecurityName("2001:db8::1::2"));
        assertNull(TLSTMUtil.formatIpAddressForTmSecurityName("1:2:3:4:5:6:7:8:9"));
        assertNull(TLSTMUtil.formatIpAddressForTmSecurityName("2001:xyz::1"));
        // the certificate contains iPAddress 2001:db8::1:
        assertEquals(new OctetString("20010db8000000000000000000000001"),
                TLSTMUtil.getIpAddressFromSubjAltName(certB.getSubjectAlternativeNames()));
    }

    @Test
    public void testFingerprintOfHashAlgorithms() throws Exception {
        assertArrayEquals(fingerprint(certA, HASH_ALGORITHM_SHA256, "SHA-256").getValue(),
                TLSTMUtil.getFingerprint(certA, HASH_ALGORITHM_SHA256).getValue());
        assertEquals("SHA-256", TLSTMUtil.getHashAlgorithmName(HASH_ALGORITHM_SHA256));
        assertNull(TLSTMUtil.getHashAlgorithmName(0));
        assertNull(TLSTMUtil.getHashAlgorithmName(7));
        assertNull(TLSTMUtil.getFingerprint(certA, 0));
    }
}
