Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions HMAC_AUTHENTICATION_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# HMAC Authentication Implementation for RegisterTMRequest

## Overview
This implementation extends the `RegisterTMRequest` message with HMAC-based authentication fields while maintaining backward compatibility with older versions.

## Changes Made

### 1. Core Protocol Changes
**File: `core/src/main/java/org/apache/seata/core/protocol/RegisterTMRequest.java`**

Added four new fields to support HMAC authentication:
- `accessKey` (String): The access key for authentication
- `digest` (String): The HMAC digest generated from the message
- `timestamp` (Long): Timestamp when the request was created
- `authVersion` (String): Version of the authentication algorithm (e.g., "V4")

Each field includes standard getter and setter methods.

### 2. Seata Serializer Codec Changes
**File: `serializer/seata-serializer-seata/src/main/java/org/apache/seata/serializer/seata/protocol/RegisterTMRequestCodec.java`**

Extended the codec to encode and decode the new HMAC fields:

**Encoding:**
- Writes each field in order: accessKey, digest, timestamp, authVersion
- Uses standard length-prefixed encoding for strings (2-byte length + UTF-8 bytes)
- Uses 8-byte long for timestamp
- Null values are properly handled

**Decoding (Backward Compatible):**
- Reads the basic fields first (version, applicationId, transactionServiceGroup, extraData)
- Checks remaining buffer size before attempting to read each HMAC field
- If buffer ends before HMAC fields, gracefully returns without error
- This allows old messages (without HMAC fields) to be decoded successfully

### 3. Protobuf Serializer Changes
**File: `serializer/seata-serializer-protobuf/src/main/resources/protobuf/org/apache/seata/protocol/transcation/registerTMRequest.proto`**

Extended the protobuf definition with new fields:
```protobuf
message RegisterTMRequestProto {
AbstractIdentifyRequestProto abstractIdentifyRequest = 1;
string accessKey = 2;
string digest = 3;
int64 timestamp = 4;
string authVersion = 5;
}
```

**File: `serializer/seata-serializer-protobuf/src/main/java/org/apache/seata/serializer/protobuf/convertor/RegisterTMRequestConvertor.java`**

Updated the convertor to handle the new fields:
- `convert2Proto`: Populates new fields if they are non-null
- `convert2Model`: Reads new fields and only sets them if they are non-empty/non-zero

### 4. Client Integration
**File: `core/src/main/java/org/apache/seata/core/rpc/netty/TmNettyRemotingClient.java`**

Updated the `getPoolKeyFunction` method to populate HMAC fields when creating RegisterTMRequest:
- Sets accessKey from configuration
- Generates digest using the configured signer
- Sets current timestamp
- Sets authVersion from signer

## Backward Compatibility

The implementation ensures backward compatibility in multiple ways:

### 1. Decode Compatibility
- **Old client → New server**: Old clients send messages without HMAC fields. The new server's codec checks for remaining bytes before reading HMAC fields and gracefully handles their absence.
- **New client → Old server**: While new clients will send HMAC fields, old servers using the old codec will only read the basic fields and ignore any additional data.

### 2. Optional Fields
All new fields are optional and can be null:
- If not set by the client, they are encoded as empty/zero values
- The decoder handles missing fields by leaving them as null

### 3. Proto3 Compatibility
Using proto3 syntax ensures that unset fields are handled gracefully with default values.

## Testing

### Unit Tests
Added comprehensive unit tests in:
1. `RegisterTMRequestSerializerTest`:
- `test_codec_with_hmac_fields()`: Tests full encode/decode with HMAC fields
- `test_backward_compatibility_old_message()`: Tests that old messages without HMAC fields decode correctly

2. `RegisterTMRequestConvertorTest`:
- `convert2Proto_withHmacFields()`: Tests protobuf conversion with HMAC fields
- `convert2Proto_backwardCompatibility()`: Tests protobuf conversion without HMAC fields

All tests verify:
- Successful encoding and decoding
- Field values are preserved correctly
- Old messages without HMAC fields are handled gracefully
- New fields remain null when not present in encoded data

## Version Compatibility Matrix

| Client Version | Server Version | Result |
|---------------|---------------|--------|
| Old (without HMAC) | Old (without HMAC) | ✅ Works - Basic fields only |
| Old (without HMAC) | New (with HMAC) | ✅ Works - Server reads basic fields, HMAC fields are null |
| New (with HMAC) | Old (without HMAC) | ✅ Works - Server reads basic fields, ignores extra data |
| New (with HMAC) | New (with HMAC) | ✅ Works - Full HMAC authentication supported |

## Conclusion

This implementation successfully extends RegisterTMRequest with HMAC authentication capabilities while maintaining full backward compatibility with existing deployments. The design allows for gradual migration from non-authenticated to authenticated communication.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public class RegisterTMRequest extends AbstractIdentifyRequest implements Serial
public static final String UDATA_TIMESTAMP = "timestamp";
public static final String UDATA_AUTH_VERSION = "authVersion";

private String accessKey;
private String digest;
private Long timestamp;
private String authVersion;

/**
* Instantiates a new Register tm request.
*/
Expand Down Expand Up @@ -80,6 +85,38 @@ public RegisterTMRequest(String applicationId, String transactionServiceGroup) {
this(applicationId, transactionServiceGroup, null);
}

public String getAccessKey() {
return accessKey;
}

public void setAccessKey(String accessKey) {
this.accessKey = accessKey;
}

public String getDigest() {
return digest;
}

public void setDigest(String digest) {
this.digest = digest;
}

public Long getTimestamp() {
return timestamp;
}

public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}

public String getAuthVersion() {
return authVersion;
}

public void setAuthVersion(String authVersion) {
this.authVersion = authVersion;
}

@Override
public short getTypeCode() {
return MessageType.TYPE_REG_CLT;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,22 @@ public void destroy() {
protected Function<String, NettyPoolKey> getPoolKeyFunction() {
return severAddress -> {
RegisterTMRequest message = new RegisterTMRequest(applicationId, transactionServiceGroup, getExtraData());

String ip = NetUtil.getLocalIp();
long timestamp = System.currentTimeMillis();
String digestSource;
if (StringUtils.isEmpty(ip)) {
digestSource = transactionServiceGroup + ",127.0.0.1," + timestamp;
} else {
digestSource = transactionServiceGroup + "," + ip + "," + timestamp;
}
String digest = signer.sign(digestSource, secretKey);

message.setAccessKey(accessKey);
message.setDigest(digest);
message.setTimestamp(timestamp);
message.setAuthVersion(signer.getSignVersion());

return new NettyPoolKey(NettyPoolKey.TransactionRole.TMROLE, severAddress, message);
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,12 @@ protected String buildUndoSQL() {
+ sqlUndoLog.getTableName()
+ " OFF;";
} else {
return " INSERT INTO " +
sqlUndoLog.getTableName() +
" (" +
insertColumns +
") VALUES (" +
insertValues +
");";
return " INSERT INTO " + sqlUndoLog.getTableName()
+ " ("
+ insertColumns
+ ") VALUES ("
+ insertValues
+ ");";
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,23 @@ public RegisterTMRequestProto convert2Proto(RegisterTMRequest registerTMRequest)
.setVersion(registerTMRequest.getVersion())
.build();

RegisterTMRequestProto result = RegisterTMRequestProto.newBuilder()
.setAbstractIdentifyRequest(abstractIdentifyRequestProto)
.build();
RegisterTMRequestProto.Builder builder =
RegisterTMRequestProto.newBuilder().setAbstractIdentifyRequest(abstractIdentifyRequestProto);

if (registerTMRequest.getAccessKey() != null) {
builder.setAccessKey(registerTMRequest.getAccessKey());
}
if (registerTMRequest.getDigest() != null) {
builder.setDigest(registerTMRequest.getDigest());
}
if (registerTMRequest.getTimestamp() != null) {
builder.setTimestamp(registerTMRequest.getTimestamp());
}
if (registerTMRequest.getAuthVersion() != null) {
builder.setAuthVersion(registerTMRequest.getAuthVersion());
}

return result;
return builder.build();
}

@Override
Expand All @@ -57,6 +69,23 @@ public RegisterTMRequest convert2Model(RegisterTMRequestProto registerTMRequestP
registerRMRequest.setTransactionServiceGroup(abstractIdentifyRequest.getTransactionServiceGroup());
registerRMRequest.setVersion(abstractIdentifyRequest.getVersion());

String accessKey = registerTMRequestProto.getAccessKey();
if (accessKey != null && !accessKey.isEmpty()) {
registerRMRequest.setAccessKey(accessKey);
}
String digest = registerTMRequestProto.getDigest();
if (digest != null && !digest.isEmpty()) {
registerRMRequest.setDigest(digest);
}
long timestamp = registerTMRequestProto.getTimestamp();
if (timestamp > 0) {
registerRMRequest.setTimestamp(timestamp);
}
String authVersion = registerTMRequestProto.getAuthVersion();
if (authVersion != null && !authVersion.isEmpty()) {
registerRMRequest.setAuthVersion(authVersion);
}

return registerRMRequest;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,8 @@ option java_package = "org.apache.seata.serializer.protobuf.generated";
// PublishRequest is a publish request.
message RegisterTMRequestProto {
AbstractIdentifyRequestProto abstractIdentifyRequest = 1;
string accessKey = 2;
string digest = 3;
int64 timestamp = 4;
string authVersion = 5;
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,56 @@ public void convert2Proto() {
assertThat((real.getExtraData())).isEqualTo(registerRMRequest.getExtraData());
assertThat((real.getApplicationId())).isEqualTo(registerRMRequest.getApplicationId());
}

@Test
public void convert2Proto_withHmacFields() {

RegisterTMRequest registerRMRequest = new RegisterTMRequest();
registerRMRequest.setVersion("2.0");
registerRMRequest.setTransactionServiceGroup("testGroup");
registerRMRequest.setExtraData("extraData");
registerRMRequest.setApplicationId("testApp");
registerRMRequest.setAccessKey("testAccessKey");
registerRMRequest.setDigest("testDigest");
registerRMRequest.setTimestamp(1234567890L);
registerRMRequest.setAuthVersion("V4");

RegisterTMRequestConvertor convertor = new RegisterTMRequestConvertor();
RegisterTMRequestProto proto = convertor.convert2Proto(registerRMRequest);
RegisterTMRequest real = convertor.convert2Model(proto);

assertThat((real.getTypeCode())).isEqualTo(registerRMRequest.getTypeCode());
assertThat((real.getVersion())).isEqualTo(registerRMRequest.getVersion());
assertThat((real.getTransactionServiceGroup())).isEqualTo(registerRMRequest.getTransactionServiceGroup());
assertThat((real.getExtraData())).isEqualTo(registerRMRequest.getExtraData());
assertThat((real.getApplicationId())).isEqualTo(registerRMRequest.getApplicationId());
assertThat((real.getAccessKey())).isEqualTo(registerRMRequest.getAccessKey());
assertThat((real.getDigest())).isEqualTo(registerRMRequest.getDigest());
assertThat((real.getTimestamp())).isEqualTo(registerRMRequest.getTimestamp());
assertThat((real.getAuthVersion())).isEqualTo(registerRMRequest.getAuthVersion());
}

@Test
public void convert2Proto_backwardCompatibility() {

RegisterTMRequest registerRMRequest = new RegisterTMRequest();
registerRMRequest.setVersion("1.0");
registerRMRequest.setTransactionServiceGroup("oldGroup");
registerRMRequest.setExtraData("oldExtra");
registerRMRequest.setApplicationId("oldApp");

RegisterTMRequestConvertor convertor = new RegisterTMRequestConvertor();
RegisterTMRequestProto proto = convertor.convert2Proto(registerRMRequest);
RegisterTMRequest real = convertor.convert2Model(proto);

assertThat((real.getTypeCode())).isEqualTo(registerRMRequest.getTypeCode());
assertThat((real.getVersion())).isEqualTo(registerRMRequest.getVersion());
assertThat((real.getTransactionServiceGroup())).isEqualTo(registerRMRequest.getTransactionServiceGroup());
assertThat((real.getExtraData())).isEqualTo(registerRMRequest.getExtraData());
assertThat((real.getApplicationId())).isEqualTo(registerRMRequest.getApplicationId());
assertThat((real.getAccessKey())).isNull();
assertThat((real.getDigest())).isNull();
assertThat((real.getTimestamp())).isNull();
assertThat((real.getAuthVersion())).isNull();
}
}
Loading
Loading