Skip to content
Merged
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
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-core'
implementation 'io.micrometer:micrometer-registry-prometheus'
//did
implementation 'org.bitcoinj:bitcoinj-core:0.15.10'
implementation 'org.json:json:20240303'


annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
1 change: 1 addition & 0 deletions did-agent
Submodule did-agent added at 890626
2 changes: 2 additions & 0 deletions src/main/java/com/doubleo/didagent/DidAgentApplication.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.doubleo.didagent;

import java.security.Security;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

Expand All @@ -8,5 +9,6 @@ public class DidAgentApplication {

public static void main(String[] args) {
SpringApplication.run(DidAgentApplication.class, args);
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
}
}
Empty file.
3 changes: 3 additions & 0 deletions src/main/java/com/doubleo/didagent/agent/AcapyAgent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.doubleo.didagent.agent;

public class AcapyAgent {}
Empty file.
12 changes: 10 additions & 2 deletions src/main/java/com/doubleo/didagent/controller/DidController.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
package com.doubleo.didagent.controller;

import com.doubleo.didagent.dto.request.DidCreateRequest;
import com.doubleo.didagent.dto.response.DidCreateResponse;
import com.doubleo.didagent.service.DidService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/dids")
@RequiredArgsConstructor
public class DidController {

private final DidService didService;

@PostMapping
public DidCreateResponse didCreate() {
return new DidCreateResponse();
public DidCreateResponse peer2DidCreate(@Valid @RequestBody DidCreateRequest request) {
return didService.createPeer2Did(request);
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.doubleo.didagent.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.List;

public record DidCreateRequest(
@NotNull List<String> routingKeys, @NotBlank String serviceEndpoint) {}
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
package com.doubleo.didagent.dto.response;

public record DidCreateResponse() {}
import jakarta.validation.constraints.NotBlank;

public record DidCreateResponse(
@NotBlank String peerDid2,
@NotBlank String signingKeyMb58,
@NotBlank String signingPrivBase58,
@NotBlank String agreementKeyMb58,
@NotBlank String x25519PrivateMb58) {}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

@Getter
@AllArgsConstructor
public enum DIDErrorCode implements BaseErrorCode {
SAMPLE_ERROR(HttpStatus.NOT_FOUND, "DID Agent API Sample Error"),
public enum DidErrorCode implements BaseErrorCode {
MALFORMED_PEER_DID(HttpStatus.BAD_REQUEST, "잘못된 형식의 DID 입니다."),
KEY_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "keypair 생성에 실패했습니다."),
;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.doubleo.didagent.global.util;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.EdECPrivateKey;
import java.security.interfaces.EdECPublicKey;
import java.util.Arrays;
import org.bitcoinj.core.Base58;
import org.bouncycastle.jcajce.interfaces.XDHPrivateKey;
import org.bouncycastle.jcajce.interfaces.XDHPublicKey;

public class Ed25519KeyGenerator {

public static KeyMaterial generate() throws Exception {

KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519");
KeyPair kp = kpg.generateKeyPair();

byte[] pubSpki = kp.getPublic().getEncoded();
byte[] rawPub = Arrays.copyOfRange(pubSpki, pubSpki.length - 32, pubSpki.length);

byte[] privPkcs8 = kp.getPrivate().getEncoded();
byte[] rawPriv = Arrays.copyOfRange(privPkcs8, privPkcs8.length - 32, privPkcs8.length);

byte[] prefixed = new byte[rawPub.length + 2];
prefixed[0] = (byte) 0xED;
prefixed[1] = 0x01;
System.arraycopy(rawPub, 0, prefixed, 2, rawPub.length);

String publicKeyBase58 = "z" + Base58.encode(prefixed);
String privateKeyBase58 = Base58.encode(rawPriv);

KeyPairGenerator xKpg = KeyPairGenerator.getInstance("X25519", "BC");
KeyPair xKp = xKpg.generateKeyPair();

byte[] xPubSpki = xKp.getPublic().getEncoded();
byte[] xRawPub = Arrays.copyOfRange(xPubSpki, xPubSpki.length - 32, xPubSpki.length);

/* multicodec: 0xEC 0x01 = X25519 public key */
byte[] xPrefixed = new byte[xRawPub.length + 2];
xPrefixed[0] = (byte) 0xEC;
xPrefixed[1] = 0x01;
System.arraycopy(xRawPub, 0, xPrefixed, 2, xRawPub.length);

String agreementKeyMb58 = "z" + Base58.encode(xPrefixed);
byte[] xPrivSpki = xKp.getPrivate().getEncoded();
// PKCS#8 또는 SPKI 형식으로 인코딩된 값이 넘어오므로, 끝 32바이트가 실제 raw private
byte[] xRawPriv = Arrays.copyOfRange(xPrivSpki, xPrivSpki.length - 32, xPrivSpki.length);

// multicodec 형식 붙이기 (0xEC 0x01 = X25519 private multicodec)
byte[] xPrivPrefixed = new byte[xRawPriv.length + 2];
xPrivPrefixed[0] = (byte) 0xEC;
xPrivPrefixed[1] = 0x01;
System.arraycopy(xRawPriv, 0, xPrivPrefixed, 2, xRawPriv.length);

// 최종적으로 multibase58 (z-prefixed) 문자열
String x25519PrivateMb58 = "z" + Base58.encode(xPrivPrefixed);

/* 반환 객체에 추가로 포함 */
return new KeyMaterial(
rawPub,
rawPriv,
publicKeyBase58,
privateKeyBase58,
agreementKeyMb58, // NEW: X25519 public multibase
(EdECPublicKey) kp.getPublic(),
(EdECPrivateKey) kp.getPrivate(),
(XDHPublicKey) xKp.getPublic(),
(XDHPrivateKey) xKp.getPrivate(),
x25519PrivateMb58);
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/doubleo/didagent/global/util/KeyMaterial.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.doubleo.didagent.global.util;

import java.security.interfaces.EdECPrivateKey;
import java.security.interfaces.EdECPublicKey;
import org.bouncycastle.jcajce.interfaces.XDHPrivateKey;
import org.bouncycastle.jcajce.interfaces.XDHPublicKey;

public record KeyMaterial(
byte[] rawEd25519Public, // 32-byte 원본 Ed25519 공개키
byte[] rawEd25519Private, // 32-byte 원본 Ed25519 비밀키
String signingKeyMb58, // 멀티코덱+멀티베이스(“z…”) Ed25519 공개키
String signingPrivBase58, // Base58 인코딩된 Ed25519 비밀키(원본 32 바이트)
String agreementKeyMb58, // 멀티코덱+멀티베이스(“z…”) X25519 공개키
EdECPublicKey signingPublic, // JCA Ed25519 PublicKey (서명용)
EdECPrivateKey signingPrivate, // JCA Ed25519 PrivateKey (서명용)
XDHPublicKey agreementPublic, // JCA X25519 PublicKey (암호화·키합의용)
XDHPrivateKey agreementPrivate, // JCA X25519 PrivateKey (암호화·키합의용)
String x25519PrivateMb58) {}
54 changes: 54 additions & 0 deletions src/main/java/com/doubleo/didagent/global/util/PeerDidUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.doubleo.didagent.global.util;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import org.bitcoinj.core.Base58;
import org.json.JSONArray;
import org.json.JSONObject;

public class PeerDidUtil {

public static String createPeerDid2(
String signingKeyMb, // z6... (Ed25519)
String agreementKeyMb, // z6L... (X25519)
List<String> routingKeys,
String serviceEndpoint) {

String enc1 = "V" + signingKeyMb; // verification key
String enc2 = "E" + agreementKeyMb; // key-agreement key
List<String> routingKeysDidUrl = PeerDidUtil.convertRoutingKeys(routingKeys);
JSONObject svc = new JSONObject();
svc.put("id", "#didcomm-0");
svc.put("t", "did-communication");
svc.put("p", 0);
svc.put("recipientKeys", new JSONArray().put("#key-1"));
if (routingKeys != null && !routingKeys.isEmpty()) {
svc.put("r", new JSONArray(routingKeysDidUrl)); // routingKeys → r
}
svc.put("s", serviceEndpoint); // serviceEndpoint → s

String enc3 =
"S"
+ Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(svc.toString().getBytes(StandardCharsets.UTF_8));

return "did:peer:2." + enc1 + "." + enc2 + "." + enc3;
}

private static String rawVerkeyToDidKey(String verkeyBase58) {
byte[] raw = Base58.decode(verkeyBase58); // 32-byte 공개키
byte[] prefixed = new byte[raw.length + 2];
prefixed[0] = (byte) 0xED; // multicodec: 0xED 0x01 = Ed25519
prefixed[1] = 0x01;
System.arraycopy(raw, 0, prefixed, 2, raw.length);

String multibase = "z" + Base58.encode(prefixed);
return "did:key:" + multibase + "#" + multibase;
}

private static List<String> convertRoutingKeys(List<String> rawKeys) {
return rawKeys.stream().map(PeerDidUtil::rawVerkeyToDidKey).toList();
}
}
42 changes: 42 additions & 0 deletions src/main/java/com/doubleo/didagent/service/DidService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.doubleo.didagent.service;

import com.doubleo.didagent.dto.request.DidCreateRequest;
import com.doubleo.didagent.dto.response.DidCreateResponse;
import com.doubleo.didagent.global.exception.CommonException;
import com.doubleo.didagent.global.exception.errorcode.DidErrorCode;
import com.doubleo.didagent.global.util.Ed25519KeyGenerator;
import com.doubleo.didagent.global.util.KeyMaterial;
import com.doubleo.didagent.global.util.PeerDidUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class DidService {

public DidCreateResponse createPeer2Did(DidCreateRequest request) {
KeyMaterial key = getKeyMaterial();
String peer2Did =
PeerDidUtil.createPeerDid2(
key.signingKeyMb58(), // Ed25519 서명 키 (V)
key.agreementKeyMb58(), // X25519 암호화 키 (E)
request.routingKeys(),
request.serviceEndpoint());
log.info("Created PeerDid2: {}", peer2Did);
return new DidCreateResponse(
peer2Did,
key.signingKeyMb58(),
key.signingPrivBase58(),
key.agreementKeyMb58(),
key.x25519PrivateMb58());
}

private KeyMaterial getKeyMaterial() throws CommonException {
try {
System.out.println(Ed25519KeyGenerator.generate());
return Ed25519KeyGenerator.generate();
} catch (Exception e) {
throw new CommonException(DidErrorCode.KEY_GENERATION_FAILED);
}
}
}
2 changes: 1 addition & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
spring:
application:
name: did-service
name: did-agent
server:
port: 8080
management:
Expand Down
11 changes: 11 additions & 0 deletions src/test/java/com/doubleo/didagent/DidAgentApplicationTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.doubleo.didagent;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class DidAgentApplicationTests {

@Test
void contextLoads() {}
}
Loading