From 76bcf4af7e4a047d6d3185e05b56ef5014146d54 Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Sun, 18 Oct 2026 23:55:50 +0200 Subject: [PATCH 1/2] Implement MD5-128 integrity (suites 11 and 12), keep xRC4 out by decision IPMI 2.0 section 13.28.4 defines MD5-128 as a plain MD5 of the user's key around the message: AuthCode = MD5(password + AuthType/Format ... Next Header + password), the password zero-padded to 20 bytes as the BMC stores it (FreeIPMI does the same). New IntegrityMd5_128 implements it; unlike the HMAC algorithms it is keyed with the password, not the SIK, so CipherSuite gains initializeAlgorithms(sik, password), which Connection calls after RAKP 4; the one-argument form keeps working for every other suite. Suites 11 and 12 are now supported (isSupported() true, ipmicli probe says so), and the default preference of IpmiClient and ipmicli -C ends with 12, 11 after the six HMAC suites, so a BMC that offers one of the six still gets the same suite as before. xRC4 (suites 4, 5, 9, 10, 13, 14, 18, 19) stays unimplemented: the specification deprecates it and no BMC requires it; the exception no longer says "not yet". Live: suite 12 opens a session on a Cisco IMC (chassis status and a 25 s sensors walk, every reply passing the integrity check). The same IMC refuses suite 11 and a Lenovo XCC refuses suite 12 at Open Session ("No Cipher Suite match"), as ipmiutil -J 11 / -J 12 does too: BMC policy, not the library. The fake BMC of the tests accepts suite 12 with its own MD5 of the password around the packet. Closes #106 Co-Authored-By: Claude Fable 5.1 --- .../org/metricshub/ipmi/cli/CliArguments.java | 2 +- .../java/org/metricshub/ipmi/cli/IpmiCli.java | 2 +- .../metricshub/ipmi/client/IpmiClient.java | 7 +- .../core/coding/security/CipherSuite.java | 44 +++++++-- .../coding/security/IntegrityMd5_128.java | 98 +++++++++++++++++++ .../ipmi/core/connection/Connection.java | 2 +- src/site/markdown/cli.md | 2 +- src/site/markdown/configuration.md | 12 ++- src/site/markdown/low-level-api.md | 2 +- src/site/markdown/migrating-from-verax.md | 2 +- src/site/markdown/preparing-the-bmc.md | 22 +++-- src/site/markdown/supported-commands.md | 9 +- src/site/markdown/troubleshooting.md | 6 +- src/site/markdown/upgrading.md | 11 +++ .../ipmi/client/IpmiClientTest.java | 7 ++ .../core/coding/security/CipherSuiteTest.java | 4 +- .../coding/security/IntegrityMd5_128Test.java | 71 ++++++++++++++ .../ipmi/core/transport/FakeRmcpPlusBmc.java | 28 ++++-- .../org/metricshub/ipmi/cli/probe-json.txt | 2 +- .../org/metricshub/ipmi/cli/probe.txt | 4 +- 20 files changed, 285 insertions(+), 52 deletions(-) create mode 100644 src/main/java/org/metricshub/ipmi/core/coding/security/IntegrityMd5_128.java create mode 100644 src/test/java/org/metricshub/ipmi/core/coding/security/IntegrityMd5_128Test.java diff --git a/src/main/java/org/metricshub/ipmi/cli/CliArguments.java b/src/main/java/org/metricshub/ipmi/cli/CliArguments.java index 38a6efc..7c297a8 100644 --- a/src/main/java/org/metricshub/ipmi/cli/CliArguments.java +++ b/src/main/java/org/metricshub/ipmi/cli/CliArguments.java @@ -72,7 +72,7 @@ enum Subcommand { static final long DEFAULT_MESSAGE_TIMEOUT_MS = 5_000L; static final int DEFAULT_RETRIES = 3; /** The preference of the client, repeated here for the usage text and the {@code probe} output. */ - static final int[] DEFAULT_CIPHER_SUITES = { 17, 3, 8, 16, 2, 7 }; + static final int[] DEFAULT_CIPHER_SUITES = { 17, 3, 8, 16, 2, 7, 12, 11 }; static final char DEFAULT_ESCAPE = '~'; static final String SUBCOMMANDS = "probe, info, chassis, power, identify, sensors, fru, sel, sol or raw"; static final String POWER_ACTIONS = "status, on, off, cycle, reset, soft or diag"; diff --git a/src/main/java/org/metricshub/ipmi/cli/IpmiCli.java b/src/main/java/org/metricshub/ipmi/cli/IpmiCli.java index 0a55d98..a67a4ba 100644 --- a/src/main/java/org/metricshub/ipmi/cli/IpmiCli.java +++ b/src/main/java/org/metricshub/ipmi/cli/IpmiCli.java @@ -1388,7 +1388,7 @@ static String usage() { + " -L, --privilege user, operator, administrator or auto (default: the lowest the subcommand\n" + " needs); auto runs at user and raises the session when a command needs more\n" + " -C, --cipher-suite [,..] One ID: use exactly this suite; several: preference order" - + " (default: 17,3,8,16,2,7)\n" + + " (default: 17,3,8,16,2,7,12,11)\n" + " -t, --timeout Deadline of the subcommand (default: 120000); for sol, of its setup\n" + " --message-timeout Per-message timeout (default: 5000)\n" + " --retries Resends of a message without reply (default: 3)\n" diff --git a/src/main/java/org/metricshub/ipmi/client/IpmiClient.java b/src/main/java/org/metricshub/ipmi/client/IpmiClient.java index 434c4f7..498f185 100644 --- a/src/main/java/org/metricshub/ipmi/client/IpmiClient.java +++ b/src/main/java/org/metricshub/ipmi/client/IpmiClient.java @@ -157,9 +157,10 @@ public final class IpmiClient implements AutoCloseable { * The cipher suites the client may choose when the builder sets none, by order of preference (IPMI 2.0 table * 22-20): those that sign every message, and encrypt it when possible (17: RAKP-HMAC-SHA256, HMAC-SHA256-128, * AES-CBC-128; 3: RAKP-HMAC-SHA1, HMAC-SHA1-96, AES-CBC-128; 8: RAKP-HMAC-MD5, HMAC-MD5-128, AES-CBC-128; then - * 16, 2 and 7, the same without encryption). + * 16, 2 and 7, the same without encryption; last 12 and 11, whose MD5-128 integrity is a plain MD5 of the + * password around the message, for the older BMCs that offer nothing better). */ - private static final int[] DEFAULT_CIPHER_SUITES = { 17, 3, 8, 16, 2, 7 }; + private static final int[] DEFAULT_CIPHER_SUITES = { 17, 3, 8, 16, 2, 7, 12, 11 }; /** * The suites without integrity (0, 1, 6, 15), which the builder refuses: the list of suites comes unauthenticated, @@ -1350,7 +1351,7 @@ public Builder privilege(PrivilegeLevel level) { /** * Sets the cipher suites the client may use, by order of preference: the first one the BMC offers is used. - * Default: 17, 3, 8, 16, 2, 7. The suites without integrity (0, 1, 6, 15) are refused. + * Default: 17, 3, 8, 16, 2, 7, 12, 11. The suites without integrity (0, 1, 6, 15) are refused. * * @param ids the suite IDs (IPMI 2.0 table 22-20) * @return this builder diff --git a/src/main/java/org/metricshub/ipmi/core/coding/security/CipherSuite.java b/src/main/java/org/metricshub/ipmi/core/coding/security/CipherSuite.java index 5d73422..1c2562d 100644 --- a/src/main/java/org/metricshub/ipmi/core/coding/security/CipherSuite.java +++ b/src/main/java/org/metricshub/ipmi/core/coding/security/CipherSuite.java @@ -139,7 +139,8 @@ public CipherSuite copy() { } /** - * Initializes algorithms contained in this {@link CipherSuite}. + * Initializes algorithms contained in this {@link CipherSuite}. A suite with MD5-128 integrity needs the password + * too: see {@link #initializeAlgorithms(byte[], byte[])}. * * @param sik * - Session Integrity Key calculated during the opening of the @@ -157,7 +158,31 @@ public void initializeAlgorithms(byte[] sik) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { - getIntegrityAlgorithm().initialize(sik); + initializeAlgorithms(sik, null); + } + + /** + * Initializes algorithms contained in this {@link CipherSuite}: the HMAC integrity algorithms and the + * confidentiality algorithm are keyed with the Session Integrity Key, MD5-128 integrity (suites 11 to 14) with the + * user's password (IPMI 2.0 section 13.28.4). + * + * @param sik the Session Integrity Key calculated during the opening of the session + * @param password the password of the user, as sent to the BMC; null for an empty password + * @throws IllegalArgumentException + * - when an algorithm of the suite is not implemented + * @throws InvalidKeyException + * - when initiation of the algorithm fails + * @throws NoSuchAlgorithmException + * - when initiation of the algorithm fails + * @throws NoSuchPaddingException + * - when initiation of the algorithm fails + */ + public void initializeAlgorithms(byte[] sik, byte[] password) + throws InvalidKeyException, + NoSuchAlgorithmException, + NoSuchPaddingException { + IntegrityAlgorithm integrity = getIntegrityAlgorithm(); + integrity.initialize(integrity.getCode() == SecurityConstants.IA_MD5_128 ? password : sik); getConfidentialityAlgorithm().initialize(sik, getAuthenticationAlgorithm()); } @@ -202,8 +227,7 @@ public IntegrityAlgorithm getIntegrityAlgorithm() { case SecurityConstants.IA_HMAC_SHA1_96: return instantiateIntegrityAlgorithm(IntegrityHmacSha1_96::new); case SecurityConstants.IA_MD5_128: - // TODO: MD5-128 - throw new IllegalArgumentException("Integrity algorithm MD5-128 is not yet implemented."); + return instantiateIntegrityAlgorithm(IntegrityMd5_128::new); case SecurityConstants.IA_HMAC_MD5_128: return instantiateIntegrityAlgorithm(IntegrityHmacMd5_128::new); case SecurityConstants.IA_HMAC_SHA256_128: @@ -237,11 +261,11 @@ public ConfidentialityAlgorithm getConfidentialityAlgorithm() { } return ca; case SecurityConstants.CA_XRC4_40: - // TODO: XRc4-40 - throw new IllegalArgumentException("Confidentiality algorithm XRC4-40 is not yet implemented."); + // xRC4 is deprecated by the IPMI 2.0 specification (section 13.29) and no current BMC requires it: not + // implemented, the suites 4, 5, 9, 10, 13, 14, 18 and 19 are reported as unsupported + throw new IllegalArgumentException("Confidentiality algorithm XRC4-40 is not implemented."); case SecurityConstants.CA_XRC4_128: - // TODO: XRc4-128 - throw new IllegalArgumentException("Confidentiality algorithm XRC4-128 is not yet implemented."); + throw new IllegalArgumentException("Confidentiality algorithm XRC4-128 is not implemented."); default: throw new IllegalArgumentException( "Invalid confidentiality algorithm."); @@ -300,8 +324,8 @@ public static List getCipherSuites(byte[] bytes) { /** * Tells whether this library implements the algorithms of this suite. The suites listed by - * {@link #getCipherSuites(byte[])} include the ones it does not implement (MD5-128 integrity, xRC4 - * confidentiality), which fail when the session is opened. + * {@link #getCipherSuites(byte[])} include the ones it does not implement (xRC4 confidentiality: suites 4, 5, 9, + * 10, 13, 14, 18 and 19), which fail when the session is opened. * * @return true when the authentication, integrity and confidentiality algorithms are all implemented */ diff --git a/src/main/java/org/metricshub/ipmi/core/coding/security/IntegrityMd5_128.java b/src/main/java/org/metricshub/ipmi/core/coding/security/IntegrityMd5_128.java new file mode 100644 index 0000000..c3753c5 --- /dev/null +++ b/src/main/java/org/metricshub/ipmi/core/coding/security/IntegrityMd5_128.java @@ -0,0 +1,98 @@ +package org.metricshub.ipmi.core.coding.security; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * IPMI Java Client + * ჻჻჻჻჻჻ + * Copyright 2023 MetricsHub + * ჻჻჻჻჻჻ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +// Name mirrors the IPMI 2.0 integrity algorithm name (MD5-128), like the HMAC-xxx-nn classes +// CHECKSTYLE.OFF: TypeName +/** + * MD5-128 integrity algorithm (IPMI 2.0 section 13.28.4): a plain MD5 of the password around the message, + * {@code AuthCode = MD5(password + AuthType/Format ... Next Header + password)}, where the password is the user's key + * (Kuid) zero-padded to 20 bytes as the BMC stores it. Unlike the HMAC algorithms, it is not keyed with the Session + * Integrity Key: {@link #initialize(byte[])} takes the password. + */ +public class IntegrityMd5_128 extends IntegrityAlgorithm { + // CHECKSTYLE.ON: TypeName + + public static final String ALGORITHM_NAME = "MD5"; + + private static final int AUTH_CODE_LENGTH = 16; + + private static final int PASSWORD_LENGTH = 20; + + private final MessageDigest md5; + + /** + * Initiates the MD5-128 integrity algorithm. + */ + public IntegrityMd5_128() { + super(""); + try { + md5 = MessageDigest.getInstance(ALGORITHM_NAME); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Algorithm " + ALGORITHM_NAME + " is not available", e); + } + } + + /** + * Keys the algorithm with the user's password (not the Session Integrity Key), zero-padded to 20 bytes. + * + * @param password the password of the user, as sent to the BMC; null for an empty password + */ + @Override + public synchronized void initialize(byte[] password) { + setSik(Arrays.copyOf(password == null ? new byte[0] : password, PASSWORD_LENGTH)); + } + + @Override + public synchronized byte[] generateAuthCode(byte[] base) { + byte[] password = getSik(); + if (password == null) { + throw new NullPointerException("Algorithm not initialized."); + } + byte[] data = base[base.length - 2] == 0 ? injectIntegrityPad(base, AUTH_CODE_LENGTH) : base; + md5.reset(); + md5.update(password); + md5.update(data); + md5.update(password); + return md5.digest(); + } + + @Override + public byte getCode() { + return SecurityConstants.IA_MD5_128; + } + + @Override + public String getAlgorithmName() { + return ALGORITHM_NAME; + } + + @Override + public int getAuthCodeLength() { + return AUTH_CODE_LENGTH; + } +} diff --git a/src/main/java/org/metricshub/ipmi/core/connection/Connection.java b/src/main/java/org/metricshub/ipmi/core/connection/Connection.java index d4cfbf4..5625af9 100644 --- a/src/main/java/org/metricshub/ipmi/core/connection/Connection.java +++ b/src/main/java/org/metricshub/ipmi/core/connection/Connection.java @@ -580,7 +580,7 @@ public int startSession( throw new ConnectionException("Session Integrity Key is null"); } - cipherSuite.initializeAlgorithms(sik); + cipherSuite.initializeAlgorithms(sik, password); lastAction = null; diff --git a/src/site/markdown/cli.md b/src/site/markdown/cli.md index c3fbfb6..a09a6ad 100644 --- a/src/site/markdown/cli.md +++ b/src/site/markdown/cli.md @@ -92,7 +92,7 @@ either option can be repeated or given a comma-separated list. | `-kf, --bmc-key-file ` | Read the hexadecimal BMC key from a file. | | `-P, --port ` | UDP port of the BMC. Default: 623. | | `-L, --privilege ` | `user`, `operator`, `administrator` or `auto` (case-insensitive). Default: the lowest level the subcommand needs (see [below](#default-privilege-per-subcommand)). | -| `-C, --cipher-suite [,...]` | One ID: use exactly this cipher suite, without discovery. Several: the preference order among the suites the BMC offers. Default: `17,3,8,16,2,7`. Suites without integrity (0, 1, 6, 15) are refused: their replies could be forged. | +| `-C, --cipher-suite [,...]` | One ID: use exactly this cipher suite, without discovery. Several: the preference order among the suites the BMC offers. Default: `17,3,8,16,2,7,12,11`. Suites without integrity (0, 1, 6, 15) are refused: their replies could be forged. | | `-t, --timeout ` | Deadline of the subcommand, in milliseconds. Default: 120000. For `sol`, it bounds the session setup and the payload activation only. | | `--message-timeout ` | Per-message timeout. Default: 5000. | | `--retries ` | Resends of a message that got no reply. Default: 3. | diff --git a/src/site/markdown/configuration.md b/src/site/markdown/configuration.md index 8f09483..fcf738a 100644 --- a/src/site/markdown/configuration.md +++ b/src/site/markdown/configuration.md @@ -16,7 +16,7 @@ try (IpmiClient bmc = IpmiClient.builder("bmc.example.com") .bmcKey(kg) // optional: two-key login (Kg), raw bytes .port(623) // default .privilege(PrivilegeLevel.Operator) // maximum privilege of the session; default User - .cipherSuites(17, 3) // preference among the suites the BMC offers; default 17, 3, 8, 16, 2, 7 + .cipherSuites(17, 3) // preference among the suites the BMC offers; default 17, 3, 8, 16, 2, 7, 12, 11 .timeout(Duration.ofMinutes(2)) // overall deadline of each operation; default 2 min .messageTimeout(Duration.ofSeconds(5)) // per-message timeout; default 5 s .retries(3) // resends of a message without reply; default 3 @@ -39,7 +39,7 @@ message timeouts, retries or keep-alive periods. Nothing is configured globally. | `credentials(user, password)` | required by every operation but `capabilities()` | [Credentials](#credentials) | | `bmcKey(byte[])` | none | [BMC key](#bmc-key) | | `privilege(PrivilegeLevel)` | `User` | [Privilege level](#privilege-level) | -| `cipherSuites(int...)`, `cipherSuite(int)` | `17, 3, 8, 16, 2, 7` | [Cipher suites](#cipher-suites) | +| `cipherSuites(int...)`, `cipherSuite(int)` | `17, 3, 8, 16, 2, 7, 12, 11` | [Cipher suites](#cipher-suites) | | `timeout(Duration)` | 2 minutes | [Timeout](#timeout) | | `messageTimeout(Duration)` | 5 seconds | [Message timeout and retries](#message-timeout-and-retries) | | `retries(int)` | `3` | [Message timeout and retries](#message-timeout-and-retries) | @@ -122,10 +122,12 @@ command, Set Session Privilege Level included: the raise is then refused once an `cipherSuites(int...)` lists the suites the client may use, by order of preference: before opening the session, the client asks the BMC which suites it offers (Get Channel Cipher Suites) and takes -the first of the list that the BMC offers. The default, `17, 3, 8, 16, 2, 7`, prefers the suites -that sign and encrypt every message (17: RAKP-HMAC-SHA256, HMAC-SHA256-128, AES-CBC-128; 3: +the first of the list that the BMC offers. The default, `17, 3, 8, 16, 2, 7, 12, 11`, prefers the +suites that sign and encrypt every message (17: RAKP-HMAC-SHA256, HMAC-SHA256-128, AES-CBC-128; 3: RAKP-HMAC-SHA1, HMAC-SHA1-96, AES-CBC-128; 8: RAKP-HMAC-MD5, HMAC-MD5-128, AES-CBC-128), then -the same without encryption. A BMC that offers none of the listed suites fails the first operation +the same without encryption, and last the MD5-128 suites (12 with AES-CBC-128, 11 without), whose +signature is a plain MD5 of the password around the message. A BMC that offers none of the listed +suites fails the first operation with `IpmiClientException: The BMC ... offers none of the cipher suites ...`. `cipherSuite(int)` uses exactly that suite without asking the BMC, which saves the Get Channel diff --git a/src/site/markdown/low-level-api.md b/src/site/markdown/low-level-api.md index ca509eb..abeae02 100644 --- a/src/site/markdown/low-level-api.md +++ b/src/site/markdown/low-level-api.md @@ -262,7 +262,7 @@ being delivered to the listeners, and its timeout is not reported. the algorithms of the [table](preparing-the-bmc.html#cipher-suites) (the OEM suites are left out), including suites the library does not implement. Choose one it implements, with integrity: **3** or **17** in practice; `CipherSuite.isSupported()` tells whether the library implements the -algorithms of a suite, and a suite with an xRC4 or MD5-128 algorithm fails when the session is +algorithms of a suite, and a suite with an xRC4 algorithm fails when the session is opened. Do not fall back to a suite without integrity (0, 1, 6, 15) just because the BMC lists nothing else: the list is not authenticated. To skip the discovery when you already know the suite, build it with `CipherSuite.forId()` and pass it to `createConnection()` (Get Channel diff --git a/src/site/markdown/migrating-from-verax.md b/src/site/markdown/migrating-from-verax.md index 637653f..4ed2ef2 100644 --- a/src/site/markdown/migrating-from-verax.md +++ b/src/site/markdown/migrating-from-verax.md @@ -56,7 +56,7 @@ Code written for the Verax library keeps working after a package rename. | Area | Change | | --- | --- | -| Cipher suites | RAKP-HMAC-SHA256 and RAKP-HMAC-MD5 authentication, HMAC-SHA256-128 and HMAC-MD5-128 integrity: suites 6 to 8 and 15 to 17 work, in addition to 0 to 3. | +| Cipher suites | RAKP-HMAC-SHA256 and RAKP-HMAC-MD5 authentication, HMAC-SHA256-128, HMAC-MD5-128 and MD5-128 integrity: suites 6 to 8, 11, 12 and 15 to 17 work, in addition to 0 to 3. | | Keep-alive | `IpmiConnector(int port, long pingPeriod)` sets the keep-alive period of the connector, or disables it (`0`). | | SDR repository | Every OEM record type (`C0h` – `FFh`, not only `C0h`) is decoded as `OemRecord`, and a record that cannot be decoded is skipped instead of aborting the walk. Truncated Get SDR replies are read again in chunks. | | Sensor records | Full, Compact and Event-Only records share `AbstractSensorRecord` ([Upgrading](upgrading.html#sensor-records)). | diff --git a/src/site/markdown/preparing-the-bmc.md b/src/site/markdown/preparing-the-bmc.md index 65eea8b..c319207 100644 --- a/src/site/markdown/preparing-the-bmc.md +++ b/src/site/markdown/preparing-the-bmc.md @@ -87,30 +87,36 @@ them). The client implements: | 7 | RAKP-HMAC-MD5 | HMAC-MD5-128 | none | yes | | 8 | RAKP-HMAC-MD5 | HMAC-MD5-128 | AES-CBC-128 | yes | | 9, 10 | RAKP-HMAC-MD5 | HMAC-MD5-128 | xRC4-128, xRC4-40 | no | -| 11 – 14 | RAKP-HMAC-MD5 | MD5-128 | none, AES-CBC-128, xRC4 | no | +| 11 | RAKP-HMAC-MD5 | MD5-128 | none | yes | +| 12 | RAKP-HMAC-MD5 | MD5-128 | AES-CBC-128 | yes | +| 13, 14 | RAKP-HMAC-MD5 | MD5-128 | xRC4-128, xRC4-40 | no | | 15 | RAKP-HMAC-SHA256 | none | none | yes | | 16 | RAKP-HMAC-SHA256 | HMAC-SHA256-128 | none | yes | | **17** | RAKP-HMAC-SHA256 | HMAC-SHA256-128 | AES-CBC-128 | **yes** | | 18, 19 | RAKP-HMAC-SHA256 | HMAC-SHA256-128 | xRC4-128, xRC4-40 | no | Suites **3** and **17** sign and encrypt every message, and every current BMC offers at least -one of them. Prefer **17** where available. +one of them. Prefer **17** where available. The xRC4 suites are not implemented: the IPMI 2.0 +specification deprecates xRC4, and no BMC requires it. MD5-128 (suites 11 and 12) is a plain MD5 of +the password around each message rather than an HMAC of the session key: it is implemented for the +older BMCs that offer nothing better, and comes last in the default preference. ### How `IpmiClient` chooses the suite -By default (`cipherSuites(17, 3, 8, 16, 2, 7)`), the client first asks the BMC for its list of -cipher suites (Get Channel Cipher Suites) when it opens its session, then picks the first of these -suites that the BMC offers: +By default (`cipherSuites(17, 3, 8, 16, 2, 7, 12, 11)`), the client first asks the BMC for its +list of cipher suites (Get Channel Cipher Suites) when it opens its session, then picks the first of +these suites that the BMC offers: 1. **17**, then **3**, then 8: messages signed and encrypted; -2. 16, then 2, then 7: messages signed, not encrypted. +2. 16, then 2, then 7: messages signed, not encrypted; +3. 12, then 11: messages signed with MD5-128, encrypted or not. A Lenovo XCC that offers suites 1 to 19 gets suite 17, a Cisco IMC that offers 0 to 14 gets suite 3. The client never chooses a suite without integrity (0, 1, 6, 15), even when the BMC offers nothing else: the list of suites is not authenticated, so an attacker on the network could otherwise make the client fall back to a session whose replies can be forged. A BMC that -offers none of the six fails the first operation with `IpmiClientException: The BMC ... offers -none of the cipher suites [17, 3, 8, 16, 2, 7] (offered: ...)`. The algorithms of a suite are those of the +offers none of the eight fails the first operation with `IpmiClientException: The BMC ... offers +none of the cipher suites [17, 3, 8, 16, 2, 7, 12, 11] (offered: ...)`. The algorithms of a suite are those of the table above, whatever the BMC's record of the suite says, and the OEM suites a BMC may list (as Cisco IMC does) are ignored. diff --git a/src/site/markdown/supported-commands.md b/src/site/markdown/supported-commands.md index b905bb8..abfea80 100644 --- a/src/site/markdown/supported-commands.md +++ b/src/site/markdown/supported-commands.md @@ -73,12 +73,11 @@ client with [`send()`](low-level-api.html#sending-a-command-through-the-client), | Algorithm | Implemented | | --- | --- | | Authentication | RAKP-none, RAKP-HMAC-SHA1, RAKP-HMAC-MD5, RAKP-HMAC-SHA256 | -| Integrity | none, HMAC-SHA1-96, HMAC-MD5-128, HMAC-SHA256-128 — **not** MD5-128 | -| Confidentiality | none, AES-CBC-128 — **not** xRC4-128 or xRC4-40 | +| Integrity | none, HMAC-SHA1-96, HMAC-MD5-128, MD5-128, HMAC-SHA256-128 | +| Confidentiality | none, AES-CBC-128 — **not** xRC4-128 or xRC4-40, which the specification deprecates | -Hence the cipher suites 0 to 3, 6 to 8, and 15 to 17; see the -[full table](preparing-the-bmc.html#cipher-suites). MD5-128 and xRC4 are tracked in -[#106](https://github.com/metricshub/ipmi-java/issues/106). +Hence the cipher suites 0 to 3, 6 to 8, 11, 12, and 15 to 17; see the +[full table](preparing-the-bmc.html#cipher-suites). ## SDR records diff --git a/src/site/markdown/troubleshooting.md b/src/site/markdown/troubleshooting.md index 70bf776..ccc8124 100644 --- a/src/site/markdown/troubleshooting.md +++ b/src/site/markdown/troubleshooting.md @@ -116,9 +116,9 @@ interface, or with `ipmitool lan set 1 cipher_privs`), or list the suites the BM `cipherSuites()` on the builder, among those the library implements. With the [low-level API](low-level-api.html#choosing-the-cipher-suite), a suite that uses an -algorithm the client does not implement fails with -`IllegalArgumentException: Confidentiality algorithm XRC4-128 is not yet implemented.` (or -MD5-128 integrity): choose a suite whose `isSupported()` is `true`. +algorithm the client does not implement (xRC4) fails with +`IllegalArgumentException: Confidentiality algorithm XRC4-128 is not implemented.`: choose a +suite whose `isSupported()` is `true`. ## Sensors or FRUs are missing diff --git a/src/site/markdown/upgrading.md b/src/site/markdown/upgrading.md index c1c6a47..802491f 100644 --- a/src/site/markdown/upgrading.md +++ b/src/site/markdown/upgrading.md @@ -7,6 +7,17 @@ description: What changes when upgrading the IPMI Java Client — from 2.0.00, f ## Upgrading from 2.0.00 +* The MD5-128 integrity algorithm (IPMI 2.0 section 13.28.4: a plain MD5 of the password around + each message) is implemented: cipher suites 11 and 12 open sessions, where 2.0.00 failed with + `IllegalArgumentException: Integrity algorithm MD5-128 is not yet implemented.`, and + `CipherSuite.isSupported()` is `true` for them. The default preference of the builder and of + `ipmicli -C` becomes `17, 3, 8, 16, 2, 7, 12, 11`: a BMC that offers one of the first six still + gets the same suite as before, and one that offers only MD5-128 suites with integrity now connects + instead of failing with *offers none of the cipher suites*. In the protocol core, + `CipherSuite.initializeAlgorithms(sik, password)` keys MD5-128 with the password; the one-argument + `initializeAlgorithms(sik)` still works for every other suite. The xRC4 suites (4, 5, 9, 10, + 13, 14, 18, 19) stay unsupported, by decision: the specification deprecates xRC4 and no BMC + requires it; their exception now says `is not implemented.` rather than `is not yet implemented.` * `privilege(PrivilegeLevel.MaximumAvailable)` is accepted by the builder, which 2.0.00 refused: the session requests the highest level the account is allowed, runs at User, and is raised with Set Session Privilege Level only when the BMC refuses a command for insufficient diff --git a/src/test/java/org/metricshub/ipmi/client/IpmiClientTest.java b/src/test/java/org/metricshub/ipmi/client/IpmiClientTest.java index afbc87f..7ba18b6 100644 --- a/src/test/java/org/metricshub/ipmi/client/IpmiClientTest.java +++ b/src/test/java/org/metricshub/ipmi/client/IpmiClientTest.java @@ -1582,6 +1582,13 @@ void privilegeAndCipherSuiteSettings() throws Exception { } assertEquals(discoveries, bmc.getCount(NETFN_APP, GET_CHANNEL_CIPHER_SUITES), "no discovery with a fixed suite"); awaitClose(bmc); + // Suite 12: RAKP-HMAC-MD5, MD5-128 integrity (the password around the message, not an HMAC of the SIK) + try (IpmiClient client = client(bmc).cipherSuite(12).build()) { + assertTrue(client.chassisStatus().powerOn()); + assertEquals(2, bmc.getAlgorithms()[0], "RAKP-HMAC-MD5: suite 12"); + assertEquals(3, bmc.getAlgorithms()[1], "MD5-128: suite 12"); + } + awaitClose(bmc); assertEquals(Collections.emptyList(), bmc.getErrors()); } } diff --git a/src/test/java/org/metricshub/ipmi/core/coding/security/CipherSuiteTest.java b/src/test/java/org/metricshub/ipmi/core/coding/security/CipherSuiteTest.java index eb9a5ab..a39cb52 100644 --- a/src/test/java/org/metricshub/ipmi/core/coding/security/CipherSuiteTest.java +++ b/src/test/java/org/metricshub/ipmi/core/coding/security/CipherSuiteTest.java @@ -92,8 +92,8 @@ void isSupportedTellsTheSuitesThisLibraryImplements() { .filter(CipherSuite::isSupported) .map(suite -> (int) suite.getId()) .collect(Collectors.toList()); - // No xRC4 (4, 5, 9, 10, 13, 14, 18, 19) and no MD5-128 integrity (11 to 14) - assertEquals(Arrays.asList(1, 2, 3, 6, 7, 8, 15, 16, 17), supported); + // No xRC4 (4, 5, 9, 10, 13, 14, 18, 19) + assertEquals(Arrays.asList(1, 2, 3, 6, 7, 8, 11, 12, 15, 16, 17), supported); assertFalse(new CipherSuite((byte) 3, (byte) 1, (byte) 1, (byte) -1).isSupported()); // no integrity algorithm assertTrue(CipherSuite.getEmpty().isSupported()); } diff --git a/src/test/java/org/metricshub/ipmi/core/coding/security/IntegrityMd5_128Test.java b/src/test/java/org/metricshub/ipmi/core/coding/security/IntegrityMd5_128Test.java new file mode 100644 index 0000000..e936044 --- /dev/null +++ b/src/test/java/org/metricshub/ipmi/core/coding/security/IntegrityMd5_128Test.java @@ -0,0 +1,71 @@ +package org.metricshub.ipmi.core.coding.security; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.metricshub.ipmi.core.coding.security.CipherSuiteTest.bytes; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +// CHECKSTYLE.OFF: TypeName +class IntegrityMd5_128Test { + // CHECKSTYLE.ON: TypeName + + /** AuthType/Format (06h), an RMCP+ header and payload, Pad Length 0, Next Header 07h: 24 bytes, no pad needed. */ + private static final byte[] BASE = bytes("06 c0 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 00 07"); + + /** MD5("password" zero-padded to 20 bytes + BASE + the same password), computed with Python's hashlib. */ + private static final byte[] AUTH_CODE = bytes("d1 27 35 fd 17 3a b0 d5 54 96 3a e5 c3 2f 91 6b"); + + @Test + void authCodeIsTheMd5OfThePaddedPasswordAroundTheMessage() { + IntegrityMd5_128 algorithm = new IntegrityMd5_128(); + algorithm.initialize("password".getBytes(StandardCharsets.US_ASCII)); + assertArrayEquals(AUTH_CODE, algorithm.generateAuthCode(BASE)); + assertArrayEquals(AUTH_CODE, algorithm.generateAuthCode(BASE), "the digest is reset between messages"); + assertEquals(16, algorithm.getAuthCodeLength()); + assertEquals(SecurityConstants.IA_MD5_128, algorithm.getCode()); + // The BMC stores the password zero-padded to 20 bytes: the padded password gives the same code + IntegrityMd5_128 padded = new IntegrityMd5_128(); + padded.initialize(Arrays.copyOf("password".getBytes(StandardCharsets.US_ASCII), 20)); + assertArrayEquals(AUTH_CODE, padded.generateAuthCode(BASE)); + } + + @Test + void integrityPadIsInjectedWhenTheBaseHasNone() { + // 22 bytes + 16 of AuthCode = 38: two FFh pad bytes make it a multiple of 4, and Pad Length becomes 2 + byte[] unpadded = bytes("06 c0 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 00 07"); + byte[] paddedBase = bytes("06 c0 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 ff ff 02 07"); + IntegrityMd5_128 algorithm = new IntegrityMd5_128(); + algorithm.initialize(new byte[0]); + assertArrayEquals(algorithm.generateAuthCode(paddedBase), algorithm.generateAuthCode(unpadded)); + } + + @Test + void anEmptyPasswordIsTwentyZeroBytes() { + IntegrityMd5_128 withNull = new IntegrityMd5_128(); + withNull.initialize(null); + IntegrityMd5_128 withZeros = new IntegrityMd5_128(); + withZeros.initialize(new byte[20]); + assertArrayEquals(withZeros.generateAuthCode(BASE), withNull.generateAuthCode(BASE)); + assertThrows(NullPointerException.class, () -> new IntegrityMd5_128().generateAuthCode(BASE)); + } + + @Test + void theSuiteKeysMd5WithThePasswordAndTheHmacSuitesWithTheSik() throws Exception { + byte[] sik = new byte[20]; + Arrays.fill(sik, (byte) 0x5a); + byte[] password = "password".getBytes(StandardCharsets.US_ASCII); + CipherSuite suite11 = CipherSuite.forId(11); + suite11.initializeAlgorithms(sik, password); + assertArrayEquals(AUTH_CODE, suite11.getIntegrityAlgorithm().generateAuthCode(BASE)); + CipherSuite suite7 = CipherSuite.forId(7); + suite7.initializeAlgorithms(sik, password); + IntegrityHmacMd5_128 hmac = new IntegrityHmacMd5_128(); + hmac.initialize(sik); + assertArrayEquals(hmac.generateAuthCode(BASE), suite7.getIntegrityAlgorithm().generateAuthCode(BASE)); + } +} diff --git a/src/test/java/org/metricshub/ipmi/core/transport/FakeRmcpPlusBmc.java b/src/test/java/org/metricshub/ipmi/core/transport/FakeRmcpPlusBmc.java index 308fc10..f6b951c 100644 --- a/src/test/java/org/metricshub/ipmi/core/transport/FakeRmcpPlusBmc.java +++ b/src/test/java/org/metricshub/ipmi/core/transport/FakeRmcpPlusBmc.java @@ -287,6 +287,9 @@ public String toString() { } } + /** The authentication, integrity and confidentiality algorithms of the suites the BMC accepts: 3, 17 and 12. */ + private static final int[][] SUITES = { { 1, 1, 1 }, { 3, 4, 1 }, { 2, 3, 1 } }; + /** * State of the session being opened or open. */ @@ -294,9 +297,12 @@ private static final class Session { private final int consoleId; private final int managedId; private final int[] algorithms; - /** HMAC of the authentication and integrity algorithms: HmacSHA1 for suite 3, HmacSHA256 for suite 17. */ + /** + * HMAC of the authentication algorithm (and of the integrity algorithm when it is an HMAC): HmacSHA1 for suite + * 3, HmacSHA256 for suite 17, HmacMD5 for suite 12. + */ private final String mac; - /** Length of the RAKP 4 Integrity Check Value and of the AuthCode: 12 for SHA1-96, 16 for SHA256-128. */ + /** Length of the RAKP 4 Integrity Check Value and of the AuthCode: 12 for SHA1-96, 16 for the others. */ private final int macLength; private byte[] rm; private byte[] rc; @@ -317,7 +323,7 @@ private Session(int consoleId, int managedId, int[] algorithms) { this.consoleId = consoleId; this.managedId = managedId; this.algorithms = algorithms; - mac = algorithms[0] == 1 ? "HmacSHA1" : "HmacSHA256"; + mac = algorithms[0] == 1 ? "HmacSHA1" : algorithms[0] == 2 ? "HmacMD5" : "HmacSHA256"; macLength = algorithms[0] == 1 ? 12 : 16; } } @@ -810,7 +816,7 @@ private byte[] openSession(byte[] request) { int[] algorithms = { request[12] & 0x3f, request[20] & 0x3f, request[28] & 0x3f }; ByteBuffer out = ByteBuffer.allocate(36).order(ByteOrder.LITTLE_ENDIAN); out.put(request[0]); - if (!Arrays.equals(algorithms, new int[] { 1, 1, 1 }) && !Arrays.equals(algorithms, new int[] { 3, 4, 1 })) { + if (Arrays.stream(SUITES).noneMatch(suite -> Arrays.equals(suite, algorithms))) { return out.put((byte) 0x11).putShort((short) 0).putInt(consoleId).array(); // no cipher suite match } Session opened = new Session(consoleId, random.nextInt(Integer.MAX_VALUE - 1) + 1, algorithms); @@ -1217,10 +1223,18 @@ private byte[] seal(Session current, int payloadType, byte[] message, int sequen /** * @return the AuthCode of a packet: HMAC-K1 of AuthType/Format up to Next Header, truncated (IPMI 2.0 section - * 13.28.4) + * 13.28.4), or for MD5-128 (suite 12) the MD5 of the password around that range */ - private static byte[] authCode(Session current, byte[] datagram, int end) throws GeneralSecurityException { - return Arrays.copyOf(hmac(current.mac, current.k1, Arrays.copyOfRange(datagram, 4, end)), current.macLength); + private byte[] authCode(Session current, byte[] datagram, int end) throws GeneralSecurityException { + byte[] range = Arrays.copyOfRange(datagram, 4, end); + if (current.algorithms[1] == 3) { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(kuid); + md5.update(range); + md5.update(kuid); + return md5.digest(); + } + return Arrays.copyOf(hmac(current.mac, current.k1, range), current.macLength); } /** diff --git a/src/test/resources/org/metricshub/ipmi/cli/probe-json.txt b/src/test/resources/org/metricshub/ipmi/cli/probe-json.txt index e032755..9b6823c 100644 --- a/src/test/resources/org/metricshub/ipmi/cli/probe-json.txt +++ b/src/test/resources/org/metricshub/ipmi/cli/probe-json.txt @@ -1 +1 @@ -{"ipmi20":true,"bmcKeyRequired":false,"anonymousLogin":false,"nullUserNames":false,"nonNullUserNames":true,"perMessageAuthentication":true,"userLevelAuthentication":true,"cipherSuites":[{"id":1,"authentication":"RAKP-HMAC-SHA1","integrity":"none","confidentiality":"none","supported":true},{"id":2,"authentication":"RAKP-HMAC-SHA1","integrity":"HMAC-SHA1-96","confidentiality":"none","supported":true},{"id":3,"authentication":"RAKP-HMAC-SHA1","integrity":"HMAC-SHA1-96","confidentiality":"AES-CBC-128","supported":true},{"id":4,"authentication":"RAKP-HMAC-SHA1","integrity":"HMAC-SHA1-96","confidentiality":"xRC4-128","supported":false},{"id":5,"authentication":"RAKP-HMAC-SHA1","integrity":"HMAC-SHA1-96","confidentiality":"xRC4-40","supported":false},{"id":6,"authentication":"RAKP-HMAC-MD5","integrity":"none","confidentiality":"none","supported":true},{"id":7,"authentication":"RAKP-HMAC-MD5","integrity":"HMAC-MD5-128","confidentiality":"none","supported":true},{"id":8,"authentication":"RAKP-HMAC-MD5","integrity":"HMAC-MD5-128","confidentiality":"AES-CBC-128","supported":true},{"id":9,"authentication":"RAKP-HMAC-MD5","integrity":"HMAC-MD5-128","confidentiality":"xRC4-128","supported":false},{"id":10,"authentication":"RAKP-HMAC-MD5","integrity":"HMAC-MD5-128","confidentiality":"xRC4-40","supported":false},{"id":11,"authentication":"RAKP-HMAC-MD5","integrity":"MD5-128","confidentiality":"none","supported":false},{"id":12,"authentication":"RAKP-HMAC-MD5","integrity":"MD5-128","confidentiality":"AES-CBC-128","supported":false},{"id":13,"authentication":"RAKP-HMAC-MD5","integrity":"MD5-128","confidentiality":"xRC4-128","supported":false},{"id":14,"authentication":"RAKP-HMAC-MD5","integrity":"MD5-128","confidentiality":"xRC4-40","supported":false},{"id":15,"authentication":"RAKP-HMAC-SHA256","integrity":"none","confidentiality":"none","supported":true},{"id":16,"authentication":"RAKP-HMAC-SHA256","integrity":"HMAC-SHA256-128","confidentiality":"none","supported":true},{"id":17,"authentication":"RAKP-HMAC-SHA256","integrity":"HMAC-SHA256-128","confidentiality":"AES-CBC-128","supported":true},{"id":18,"authentication":"RAKP-HMAC-SHA256","integrity":"HMAC-SHA256-128","confidentiality":"xRC4-128","supported":false},{"id":19,"authentication":"RAKP-HMAC-SHA256","integrity":"HMAC-SHA256-128","confidentiality":"xRC4-40","supported":false}],"chosenCipherSuite":17} +{"ipmi20":true,"bmcKeyRequired":false,"anonymousLogin":false,"nullUserNames":false,"nonNullUserNames":true,"perMessageAuthentication":true,"userLevelAuthentication":true,"cipherSuites":[{"id":1,"authentication":"RAKP-HMAC-SHA1","integrity":"none","confidentiality":"none","supported":true},{"id":2,"authentication":"RAKP-HMAC-SHA1","integrity":"HMAC-SHA1-96","confidentiality":"none","supported":true},{"id":3,"authentication":"RAKP-HMAC-SHA1","integrity":"HMAC-SHA1-96","confidentiality":"AES-CBC-128","supported":true},{"id":4,"authentication":"RAKP-HMAC-SHA1","integrity":"HMAC-SHA1-96","confidentiality":"xRC4-128","supported":false},{"id":5,"authentication":"RAKP-HMAC-SHA1","integrity":"HMAC-SHA1-96","confidentiality":"xRC4-40","supported":false},{"id":6,"authentication":"RAKP-HMAC-MD5","integrity":"none","confidentiality":"none","supported":true},{"id":7,"authentication":"RAKP-HMAC-MD5","integrity":"HMAC-MD5-128","confidentiality":"none","supported":true},{"id":8,"authentication":"RAKP-HMAC-MD5","integrity":"HMAC-MD5-128","confidentiality":"AES-CBC-128","supported":true},{"id":9,"authentication":"RAKP-HMAC-MD5","integrity":"HMAC-MD5-128","confidentiality":"xRC4-128","supported":false},{"id":10,"authentication":"RAKP-HMAC-MD5","integrity":"HMAC-MD5-128","confidentiality":"xRC4-40","supported":false},{"id":11,"authentication":"RAKP-HMAC-MD5","integrity":"MD5-128","confidentiality":"none","supported":true},{"id":12,"authentication":"RAKP-HMAC-MD5","integrity":"MD5-128","confidentiality":"AES-CBC-128","supported":true},{"id":13,"authentication":"RAKP-HMAC-MD5","integrity":"MD5-128","confidentiality":"xRC4-128","supported":false},{"id":14,"authentication":"RAKP-HMAC-MD5","integrity":"MD5-128","confidentiality":"xRC4-40","supported":false},{"id":15,"authentication":"RAKP-HMAC-SHA256","integrity":"none","confidentiality":"none","supported":true},{"id":16,"authentication":"RAKP-HMAC-SHA256","integrity":"HMAC-SHA256-128","confidentiality":"none","supported":true},{"id":17,"authentication":"RAKP-HMAC-SHA256","integrity":"HMAC-SHA256-128","confidentiality":"AES-CBC-128","supported":true},{"id":18,"authentication":"RAKP-HMAC-SHA256","integrity":"HMAC-SHA256-128","confidentiality":"xRC4-128","supported":false},{"id":19,"authentication":"RAKP-HMAC-SHA256","integrity":"HMAC-SHA256-128","confidentiality":"xRC4-40","supported":false}],"chosenCipherSuite":17} diff --git a/src/test/resources/org/metricshub/ipmi/cli/probe.txt b/src/test/resources/org/metricshub/ipmi/cli/probe.txt index 420dfe0..b88f996 100644 --- a/src/test/resources/org/metricshub/ipmi/cli/probe.txt +++ b/src/test/resources/org/metricshub/ipmi/cli/probe.txt @@ -17,8 +17,8 @@ Cipher suites 8 RAKP-HMAC-MD5 HMAC-MD5-128 AES-CBC-128 supported 9 RAKP-HMAC-MD5 HMAC-MD5-128 xRC4-128 not implemented 10 RAKP-HMAC-MD5 HMAC-MD5-128 xRC4-40 not implemented - 11 RAKP-HMAC-MD5 MD5-128 none not implemented - 12 RAKP-HMAC-MD5 MD5-128 AES-CBC-128 not implemented + 11 RAKP-HMAC-MD5 MD5-128 none supported + 12 RAKP-HMAC-MD5 MD5-128 AES-CBC-128 supported 13 RAKP-HMAC-MD5 MD5-128 xRC4-128 not implemented 14 RAKP-HMAC-MD5 MD5-128 xRC4-40 not implemented 15 RAKP-HMAC-SHA256 none none supported From b56c51019cc74afffa85f4c17ad0d66a3ec6baa2 Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Mon, 19 Oct 2026 00:05:17 +0200 Subject: [PATCH 2/2] Refuse MD5-128 suites in initializeAlgorithms(sik): the password is required The one-argument initializer treated the missing password as an empty one and signed with twenty zero bytes, so a low-level caller using suite 11 or 12 with the old API got invalid AuthCodes instead of an error (Codex review). It now throws IllegalArgumentException pointing at initializeAlgorithms(sik, password). Co-Authored-By: Claude Fable 5.1 --- .../ipmi/core/coding/security/CipherSuite.java | 10 +++++++--- src/site/markdown/upgrading.md | 3 ++- .../core/coding/security/IntegrityMd5_128Test.java | 3 +++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/metricshub/ipmi/core/coding/security/CipherSuite.java b/src/main/java/org/metricshub/ipmi/core/coding/security/CipherSuite.java index 1c2562d..782487d 100644 --- a/src/main/java/org/metricshub/ipmi/core/coding/security/CipherSuite.java +++ b/src/main/java/org/metricshub/ipmi/core/coding/security/CipherSuite.java @@ -139,14 +139,14 @@ public CipherSuite copy() { } /** - * Initializes algorithms contained in this {@link CipherSuite}. A suite with MD5-128 integrity needs the password - * too: see {@link #initializeAlgorithms(byte[], byte[])}. + * Initializes algorithms contained in this {@link CipherSuite}. A suite with MD5-128 integrity (11 to 14) is keyed + * with the password, which this method does not have: use {@link #initializeAlgorithms(byte[], byte[])} for it. * * @param sik * - Session Integrity Key calculated during the opening of the * session or user password if 'one-key' logins are enabled. * @throws IllegalArgumentException - * - when an algorithm of the suite is not implemented + * - when an algorithm of the suite is not implemented, or when the suite uses MD5-128 integrity * @throws InvalidKeyException * - when initiation of the algorithm fails * @throws NoSuchAlgorithmException @@ -158,6 +158,10 @@ public void initializeAlgorithms(byte[] sik) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { + if (integrityAlgorithm == SecurityConstants.IA_MD5_128) { + throw new IllegalArgumentException( + "Integrity algorithm MD5-128 is keyed with the password: use initializeAlgorithms(sik, password)"); + } initializeAlgorithms(sik, null); } diff --git a/src/site/markdown/upgrading.md b/src/site/markdown/upgrading.md index 802491f..3bb2072 100644 --- a/src/site/markdown/upgrading.md +++ b/src/site/markdown/upgrading.md @@ -15,7 +15,8 @@ description: What changes when upgrading the IPMI Java Client — from 2.0.00, f gets the same suite as before, and one that offers only MD5-128 suites with integrity now connects instead of failing with *offers none of the cipher suites*. In the protocol core, `CipherSuite.initializeAlgorithms(sik, password)` keys MD5-128 with the password; the one-argument - `initializeAlgorithms(sik)` still works for every other suite. The xRC4 suites (4, 5, 9, 10, + `initializeAlgorithms(sik)` still works for every other suite and refuses the MD5-128 suites with + `IllegalArgumentException` rather than signing with an empty password. The xRC4 suites (4, 5, 9, 10, 13, 14, 18, 19) stay unsupported, by decision: the specification deprecates xRC4 and no BMC requires it; their exception now says `is not implemented.` rather than `is not yet implemented.` * `privilege(PrivilegeLevel.MaximumAvailable)` is accepted by the builder, which 2.0.00 refused: diff --git a/src/test/java/org/metricshub/ipmi/core/coding/security/IntegrityMd5_128Test.java b/src/test/java/org/metricshub/ipmi/core/coding/security/IntegrityMd5_128Test.java index e936044..3316abe 100644 --- a/src/test/java/org/metricshub/ipmi/core/coding/security/IntegrityMd5_128Test.java +++ b/src/test/java/org/metricshub/ipmi/core/coding/security/IntegrityMd5_128Test.java @@ -62,6 +62,9 @@ void theSuiteKeysMd5WithThePasswordAndTheHmacSuitesWithTheSik() throws Exception CipherSuite suite11 = CipherSuite.forId(11); suite11.initializeAlgorithms(sik, password); assertArrayEquals(AUTH_CODE, suite11.getIntegrityAlgorithm().generateAuthCode(BASE)); + // Without the password, the SIK-only initializer refuses the suite rather than signing with an empty password + assertThrows(IllegalArgumentException.class, () -> CipherSuite.forId(12).initializeAlgorithms(sik)); + CipherSuite.forId(8).initializeAlgorithms(sik); CipherSuite suite7 = CipherSuite.forId(7); suite7.initializeAlgorithms(sik, password); IntegrityHmacMd5_128 hmac = new IntegrityHmacMd5_128();