diff --git a/build.gradle b/build.gradle index d77c776..1fbae36 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/did-agent b/did-agent new file mode 160000 index 0000000..8906266 --- /dev/null +++ b/did-agent @@ -0,0 +1 @@ +Subproject commit 8906266b20b0045cde2d8631ae01474370c66b04 diff --git a/src/main/java/com/doubleo/didagent/DidAgentApplication.java b/src/main/java/com/doubleo/didagent/DidAgentApplication.java index cec8ecb..3530511 100644 --- a/src/main/java/com/doubleo/didagent/DidAgentApplication.java +++ b/src/main/java/com/doubleo/didagent/DidAgentApplication.java @@ -1,5 +1,6 @@ package com.doubleo.didagent; +import java.security.Security; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -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()); } } diff --git a/src/main/java/com/doubleo/didagent/agent/.gitkeep b/src/main/java/com/doubleo/didagent/agent/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/doubleo/didagent/agent/AcapyAgent.java b/src/main/java/com/doubleo/didagent/agent/AcapyAgent.java new file mode 100644 index 0000000..01205a2 --- /dev/null +++ b/src/main/java/com/doubleo/didagent/agent/AcapyAgent.java @@ -0,0 +1,3 @@ +package com.doubleo.didagent.agent; + +public class AcapyAgent {} diff --git a/src/main/java/com/doubleo/didagent/controller/.gitkeep b/src/main/java/com/doubleo/didagent/controller/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/doubleo/didagent/controller/DidController.java b/src/main/java/com/doubleo/didagent/controller/DidController.java index 73d2627..73384f5 100644 --- a/src/main/java/com/doubleo/didagent/controller/DidController.java +++ b/src/main/java/com/doubleo/didagent/controller/DidController.java @@ -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); } } diff --git a/src/main/java/com/doubleo/didagent/dto/request/.gitkeep b/src/main/java/com/doubleo/didagent/dto/request/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/doubleo/didagent/dto/request/DidCreateRequest.java b/src/main/java/com/doubleo/didagent/dto/request/DidCreateRequest.java new file mode 100644 index 0000000..a947a8b --- /dev/null +++ b/src/main/java/com/doubleo/didagent/dto/request/DidCreateRequest.java @@ -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 routingKeys, @NotBlank String serviceEndpoint) {} diff --git a/src/main/java/com/doubleo/didagent/dto/response/DidCreateResponse.java b/src/main/java/com/doubleo/didagent/dto/response/DidCreateResponse.java index 9225cf5..1f9e80e 100644 --- a/src/main/java/com/doubleo/didagent/dto/response/DidCreateResponse.java +++ b/src/main/java/com/doubleo/didagent/dto/response/DidCreateResponse.java @@ -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) {} diff --git a/src/main/java/com/doubleo/didagent/global/exception/errorcode/DIDErrorCode.java b/src/main/java/com/doubleo/didagent/global/exception/errorcode/DidErrorCode.java similarity index 59% rename from src/main/java/com/doubleo/didagent/global/exception/errorcode/DIDErrorCode.java rename to src/main/java/com/doubleo/didagent/global/exception/errorcode/DidErrorCode.java index 9ffd69c..7c3c156 100644 --- a/src/main/java/com/doubleo/didagent/global/exception/errorcode/DIDErrorCode.java +++ b/src/main/java/com/doubleo/didagent/global/exception/errorcode/DidErrorCode.java @@ -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; diff --git a/src/main/java/com/doubleo/didagent/global/util/Ed25519KeyGenerator.java b/src/main/java/com/doubleo/didagent/global/util/Ed25519KeyGenerator.java new file mode 100644 index 0000000..31737e2 --- /dev/null +++ b/src/main/java/com/doubleo/didagent/global/util/Ed25519KeyGenerator.java @@ -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); + } +} diff --git a/src/main/java/com/doubleo/didagent/global/util/KeyMaterial.java b/src/main/java/com/doubleo/didagent/global/util/KeyMaterial.java new file mode 100644 index 0000000..6253541 --- /dev/null +++ b/src/main/java/com/doubleo/didagent/global/util/KeyMaterial.java @@ -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) {} diff --git a/src/main/java/com/doubleo/didagent/global/util/PeerDidUtil.java b/src/main/java/com/doubleo/didagent/global/util/PeerDidUtil.java new file mode 100644 index 0000000..a8b32c4 --- /dev/null +++ b/src/main/java/com/doubleo/didagent/global/util/PeerDidUtil.java @@ -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 routingKeys, + String serviceEndpoint) { + + String enc1 = "V" + signingKeyMb; // verification key + String enc2 = "E" + agreementKeyMb; // key-agreement key + List 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 convertRoutingKeys(List rawKeys) { + return rawKeys.stream().map(PeerDidUtil::rawVerkeyToDidKey).toList(); + } +} diff --git a/src/main/java/com/doubleo/didagent/service/DidService.java b/src/main/java/com/doubleo/didagent/service/DidService.java new file mode 100644 index 0000000..9f462e5 --- /dev/null +++ b/src/main/java/com/doubleo/didagent/service/DidService.java @@ -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); + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index bd4a304..62328f4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,6 @@ spring: application: - name: did-service + name: did-agent server: port: 8080 management: diff --git a/src/test/java/com/doubleo/didagent/DidAgentApplicationTests.java b/src/test/java/com/doubleo/didagent/DidAgentApplicationTests.java new file mode 100644 index 0000000..89ae8b0 --- /dev/null +++ b/src/test/java/com/doubleo/didagent/DidAgentApplicationTests.java @@ -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() {} +}