tanakahdaのプログラマ手帳

プログラミングとかソフトウェア開発とかの備忘録

OpenBadges 3.0 + Linked Data Proof + OID4VP (minimal, educational demo) By ChatGTP@Java

package com.tanakahda;

import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

//OpenBadges 3.0 + Linked Data Proof + OID4VP (minimal, educational demo)
//---------------------------------------------------------------
//目的: 典型アーキテクチャを Java だけで通しで理解するための最小サンプル。
//- Issuer: OB3のVCを発行(LDPで署名)
//- Wallet: VCを保持し、VerifierのOID4VPリクエストに応答してVPを作成
//- Verifier: presentation_definitionで要件提示 → 返送されたVP内VCのLDP検証 + トラスト判定
//
//重要な注意:
//1) これは“動作イメージ用”の最小コードです。実運用するには以下が必要です:
//  - 本物の LDP 実装(例: data-integrity-java)に差し替え
//  - JSON-LD 正規化/ @context 解決(例: Titanium JSON-LD)
//  - OID4VPの実フロー(OAuth2/OIDC: 認可リクエスト、DPoP、state/nonce、PKCE など)
//  - DID解決と verificationMethod 公開
//  - 失効/ステータス/トラストリスト運用
//2) 下記の LdpCrypto はデモ用の“ダミー実装”です。署名/検証は疑似化しています。
//  実機能を使う場合は TODO 部分に本物のライブラリ呼び出しを入れてください。
//
//依存を最小化するため、標準ライブラリ + Jackson (com.fasterxml.jackson.core:jackson-databind) を想定。
//Maven依存例:
//<dependency>
//<groupId>com.fasterxml.jackson.core</groupId>
//<artifactId>jackson-databind</artifactId>
//<version>2.17.1</version>
//</dependency>
//
//JSON-LD/LDPの本番実装に切り替える場合の参考(ライブラリ例):
//- Titanium JSON-LD: com.apicatalog:titanium-json-ld
//- Data Integrity / Linked Data Proof: (例)data-integrity-java
//
//---------------------------------------------------------------

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

public class Ob3LdpOid4vpDemo {

// === 共通: JSONユーティリティ ===
static final ObjectMapper M = new ObjectMapper();

 // === 疑似 LDP 署名/検証 ===
 static class LdpCrypto {
     private final KeyPair issuerKeyPair; // デモ用: 発行者の鍵

     LdpCrypto(KeyPair kp) { this.issuerKeyPair = kp; }

     // デモ: JSONをcanonicalizeした“つもり”のバイト列を作り、Ed25519風に署名する体裁
     // 実運用では JSON-LD Canonicalization + Data Integrity (eddsa-rdfc-2022) 等を使用
     public ObjectNode sign(ObjectNode unsignedVc, String verificationMethodDidUrl) throws Exception {
         // proof を除いた canonical bytes (ダミー)
         byte[] canonical = canonicalizeWithoutProof(unsignedVc);
         byte[] sig = signBytes(canonical, issuerKeyPair.getPrivate());

         ObjectNode proof = M.createObjectNode();
         proof.put("type", "DataIntegrityProof");
         proof.put("created", Instant.now().toString());
         proof.put("proofPurpose", "assertionMethod");
         proof.put("verificationMethod", verificationMethodDidUrl);
         proof.put("cryptosuite", "eddsa-rdfc-2022");
         proof.put("proofValue", Base64.getUrlEncoder().withoutPadding().encodeToString(sig));

         ObjectNode signed = unsignedVc.deepCopy();
         signed.set("proof", proof);
         return signed;
     }

     public boolean verify(ObjectNode signedVc) throws Exception {
         if (!signedVc.has("proof")) return false;
         ObjectNode proof = (ObjectNode) signedVc.get("proof");
         byte[] sig = Base64.getUrlDecoder().decode(proof.get("proofValue").asText());

         // proof を取り除いた canonical bytes (ダミー)
         ObjectNode copy = signedVc.deepCopy();
         copy.remove("proof");
         byte[] canonical = canonicalizeWithoutProof(copy);

         return verifyBytes(canonical, sig, issuerKeyPair.getPublic());
     }

     private static byte[] canonicalizeWithoutProof(ObjectNode obj) throws Exception {
         // デモ: 安定化のためフィールド名で再帰ソート → UTF-8 bytes
         // 本番は JSON-LD 正規化 (URDNA2015等) が必須
         String normalized = sortJson(obj).toString();
         return normalized.getBytes(StandardCharsets.UTF_8);
     }

     private static JsonNode sortJson(JsonNode node) {
         if (node.isObject()) {
             ObjectNode in = (ObjectNode) node;
             ObjectNode out = M.createObjectNode();
             List<String> names = new ArrayList<>();
             in.fieldNames().forEachRemaining(names::add);
             Collections.sort(names);
             for (String name : names) {
                 if ("proof".equals(name)) continue; // 除外(署名対象外)
                 out.set(name, sortJson(in.get(name)));
             }
             return out;
         } else if (node.isArray()) {
             ArrayNode in = (ArrayNode) node;
             ArrayNode out = M.createArrayNode();
             for (JsonNode item : in) out.add(sortJson(item));
             return out;
         } else {
             return node; // 値はそのまま
         }
     }

     private static byte[] signBytes(byte[] data, PrivateKey sk) throws Exception {
         Signature s = Signature.getInstance("SHA256withRSA"); // デモではRSAに
         s.initSign(sk);
         s.update(data);
         return s.sign();
     }

     private static boolean verifyBytes(byte[] data, byte[] sig, PublicKey pk) throws Exception {
         Signature s = Signature.getInstance("SHA256withRSA");
         s.initVerify(pk);
         s.update(data);
         return s.verify(sig);
     }
 }

 // === Issuer: OB3 VC を LDP で発行 ===
 static class IssuerService {
     private final String issuerDid; // 例: did:web:issuer.example.org
     private final String verificationMethod; // 例: did:web:issuer.example.org#keys-1
     private final LdpCrypto ldp;

     IssuerService(String issuerDid, String verificationMethod, LdpCrypto ldp) {
         this.issuerDid = issuerDid;
         this.verificationMethod = verificationMethod;
         this.ldp = ldp;
     }

     public ObjectNode issueOb3Credential(String subjectDid, String achievementId) throws Exception {
         ObjectNode vc = M.createObjectNode();
         ArrayNode ctx = M.createArrayNode();
         ctx.add("https://www.w3.org/ns/credentials/v2");
         ctx.add("https://purl.imsglobal.org/spec/vc/ob/vocab.jsonld");
         vc.set("@context", ctx);

         ArrayNode types = M.createArrayNode();
         types.add("VerifiableCredential");
         types.add("OpenBadgeCredential");
         vc.set("type", types);

         // issuer
         ObjectNode issuer = M.createObjectNode();
         issuer.put("id", issuerDid);
         vc.set("issuer", issuer);

         // credentialSubject
         ObjectNode cs = M.createObjectNode();
         cs.put("id", subjectDid);
         cs.put("achievement", achievementId);
         vc.set("credentialSubject", cs);

         vc.put("validFrom", Instant.now().toString());

         // LDP 署名付与
         return ldp.sign(vc, verificationMethod);
     }
 }

 // === Wallet: VC を保持し、OID4VP に応答して VP を作る ===
 static class WalletService {
     private final List<ObjectNode> vcs = new ArrayList<>();

     public void store(ObjectNode vc) { vcs.add(vc); }

     // presentation_definition を見て要件に合う VC を選び、VP を作成
     public ObjectNode handleOid4vpRequest(ObjectNode vpRequest) {
         // 1) presentation_definition の要件を単純化して判定(デモ)
         String requiredType = vpRequest.at("/presentation_definition/input_descriptors/0/constraints/fields/0/filter/const").asText("OpenBadgeCredential");

         Optional<ObjectNode> match = vcs.stream().filter(vc -> {
             for (JsonNode t : vc.withArray("type")) {
                 if (t.asText().equals(requiredType)) return true;
             }
             return false;
         }).findFirst();

         if (match.isEmpty()) throw new RuntimeException("No matching VC in wallet");

         ObjectNode vp = M.createObjectNode();
         ArrayNode ctx = M.createArrayNode();
         ctx.add("https://www.w3.org/ns/credentials/v2");
         vp.set("@context", ctx);

         ArrayNode types = M.createArrayNode();
         types.add("VerifiablePresentation");
         vp.set("type", types);

         ArrayNode arr = M.createArrayNode();
         arr.add(match.get());
         vp.set("verifiableCredential", arr);

         // DPoP/nonce の応答(デモ: 要求のnonceをそのまま返す)
         String nonce = vpRequest.path("nonce").asText(null);
         if (nonce != null) vp.put("nonce", nonce);

         return vp;
     }
 }

 // === Verifier: PEX で要件提示 → VP受領 → VC の LDP 検証 + トラスト判定 ===
 static class VerifierService {
     private final Set<String> trustedIssuers; // トラストポリシー(発行者DIDの許可リスト)
     private final LdpCrypto ldpForVerify;     // デモ: 発行者公開鍵を保持している想定

     VerifierService(Set<String> trustedIssuers, LdpCrypto ldpForVerify) {
         this.trustedIssuers = trustedIssuers;
         this.ldpForVerify = ldpForVerify;
     }

     // OID4VP: presentation_definition を作る(デモ用に最小)
     public ObjectNode buildVpRequest(String requiredType, String nonce) {
         ObjectNode req = M.createObjectNode();
         ObjectNode pd = M.createObjectNode();
         ArrayNode ids = M.createArrayNode();
         ObjectNode id0 = M.createObjectNode();
         ObjectNode constraints = M.createObjectNode();
         ArrayNode fields = M.createArrayNode();
         ObjectNode f0 = M.createObjectNode();
         ObjectNode filter = M.createObjectNode();

         // デモ: "type" に requiredType が含まれる VC を要求する
         f0.put("path", "$.type"); // 簡略化
         filter.put("const", requiredType);
         f0.set("filter", filter);
         fields.add(f0);
         constraints.set("fields", fields);
         id0.set("constraints", constraints);
         ids.add(id0);
         pd.set("input_descriptors", ids);
         req.set("presentation_definition", pd);
         req.put("nonce", nonce);
         req.put("client_id", "https://verifier.example.org");
         return req;
     }

     // 返送された VP を検証
     public boolean verifyVp(ObjectNode vp) throws Exception {
         // 1) nonce チェック(デモ)
         if (!vp.has("nonce")) return false; // 本来はリクエスト発行時の値と照合

         // 2) VC 抜き出し
         ArrayNode vcs = (ArrayNode) vp.get("verifiableCredential");
         if (vcs == null || vcs.isEmpty()) return false;
         ObjectNode vc = (ObjectNode) vcs.get(0);

         // 3) LDP 署名検証
         boolean sigOk = ldpForVerify.verify(vc);
         if (!sigOk) return false;

         // 4) 発行者トラスト判定
         String issuerId = vc.path("issuer").path("id").asText();
         return trustedIssuers.contains(issuerId);
     }
 }

 /**
  * Main method
  * @param args
  * @throws Exception
  */
 public static void main(String[] args) throws Exception {
     // === デモの流れ ===

     // 鍵ペア(デモ: RSA。実運用は Ed25519 + Data Integrity を推奨)
     KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
     kpg.initialize(2048);
     KeyPair issuerKeys = kpg.generateKeyPair();

     // LDP 疑似実装をセット
     LdpCrypto ldp = new LdpCrypto(issuerKeys);

     // Issuer
     IssuerService issuer = new IssuerService(
             "did:web:issuer.example.org",
             "did:web:issuer.example.org#keys-1",
             ldp
     );

     // Wallet
     WalletService wallet = new WalletService();

     // Verifier
     VerifierService verifier = new VerifierService(
             new HashSet<>(List.of("did:web:issuer.example.org")),
             ldp // デモでは発行者の公開鍵を内包
     );

     // 1) Issuer が OB3 VC を LDP 署名で発行
     ObjectNode vc = issuer.issueOb3Credential(
             "did:key:z6Mkwalletsubject",
             "https://issuer.example.org/achievements/java-basic"
     );
     System.out.println("[Issuer] VC (signed):\n" + M.writerWithDefaultPrettyPrinter().writeValueAsString(vc));

     // 2) Wallet が VC を保存
     wallet.store(vc);

     // 3) Verifier が OID4VP の VP リクエスト(PEX: OpenBadgeCredential を要求)
     ObjectNode vpReq = verifier.buildVpRequest("OpenBadgeCredential", UUID.randomUUID().toString());
     System.out.println("\n[Verifier] VP Request (presentation_definition):\n" + M.writerWithDefaultPrettyPrinter().writeValueAsString(vpReq));

     // 4) Wallet が要件にマッチする VC を選び、VP を作成して返送
     ObjectNode vp = wallet.handleOid4vpRequest(vpReq);
     System.out.println("\n[Wallet] VP Response:\n" + M.writerWithDefaultPrettyPrinter().writeValueAsString(vp));

     // 5) Verifier が VP 内の VC を検証(LDP 署名 + 発行者トラスト)
     boolean ok = verifier.verifyVp(vp);
     System.out.println("\n[Verifier] VP verification result: " + ok);
 }
}
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>OB3-java-example</groupId>
  <artifactId>OB3-java-example</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <build>
    <sourceDirectory>src</sourceDirectory>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <release>23</release>
        </configuration>
      </plugin>
    </plugins>
  </build>
    <dependencies>
        <!-- Jackson Core -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.17.1</version>
        </dependency>

        <!-- Jackson Databind(ObjectMapper など) -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.17.1</version>
        </dependency>

        <!-- Jackson Annotations(@JsonProperty など) -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.17.1</version>
        </dependency>
    </dependencies>
</project>

Mozcで半角/全角キーによるIME有効化/無効化を設定する@Other

Windows10のサポートが切れたのでUbuntu24.04にOSを載せ替え

IMEの切り替え方法をメモ

設定 > キーボード > 入力ソース > 日本語(Mozc) > 3点ボタン > 設定 > キー設定 > カスタム > 編集 Mozcキー設定で、「モードを直接入力」 、「入力キーをHankaku/Zenkaku」、「コマンドをIMEを有効化」で適用する ※「コマンドをIMEを無効化」は初期設定済みだったと思うがなかったら同じ要領で追加

Applications/Docker.app' is not there.

以前にDockerを使わなくなったので削除したけど、再び入り用になりbrewでインストールしたら下記のエラーがでた

brew install --cask docker
Error: docker: It seems the App source '/Applications/Docker.app' is not there.

このエラーを回避するためには下記を実行する

brew uninstall --cask docker

johnjago.com

SLF4J + Logbackのメモ@Java

pom.xml

        <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.13</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.5.6</version>
        </dependency>

logback.xml は src/main/resourceに配置

<configuration>
 
    <property name="outputDir" value="/Users/tanakahda/Desktop/App/logs/" />
    <property name="fileName" value="app" />
    <property name="format1" value="%d{yyyy/MM/dd HH:mm:ss.SSS} [%-4p] [%c] %m%n" />
 
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${outputDir}${fileName}.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>7</maxHistory>
        </rollingPolicy>
 
        <encoder>
            <pattern>${format1}</pattern>
        </encoder>
    </appender>
 
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${format1}</pattern>
        </encoder>
    </appender>
 
    <root level="debug">
        <appender-ref ref="FILE" />
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

Java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class Main {
 
    /** ログ出力クラス */
    private static Logger _log = LoggerFactory.getLogger(Main.class);
 
    public static void main(String[] args) {
 
        _log.info("処理を開始します。");
 
        _log.info("処理を終了しました。");
    }
   

結果:

2024/07/06 00:55:56.363 [INFO] [com.tanakahda.Main] 処理を開始します。
2024/07/06 00:55:57.753 [INFO] [com.tanakahda.Main] 処理を終了しました。

入出力でフォルダを再帰的に操作する@Java

Files#walkFileTreeは、ディレクトリ構造を再帰的に走査する。walkFileTreeメソッドの2つ目の引数にFileVisitorインターフェースの実装をセットする。

   /**
    * 指定したディレクトリ配下をすべて削除します。
    * 
    * @param dir
    * @throws IOException
    */
    public static void deleteAll(Path dir) throws IOException {

        Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Files.delete(file);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                if (exc != null) {
                    throw exc;
                }

                Files.delete(dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }
   /**
    * srcディレクトリ配下をdestディレクトリへコピーします。
    * 
    * @param src コピー元
    * @param dest コピー先
    * @throws IOException
    */
    public static void copyAll(Path src, Path dest) throws IOException {

        Files.walkFileTree(src, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    Path targetFile = dest.resolve(src.relativize(file));
                    Path parentDir = targetFile.getParent();
                    Files.createDirectories(parentDir);
                    Files.copy(file, targetFile, StandardCopyOption.REPLACE_EXISTING);
                    return FileVisitResult.CONTINUE;
                }
             }
        );
    }

javax.tools.JavaCompilerでコンパイルするときにクラスパスを指定する@Java

javax.tools.JavaCompilerでコンパイルするときに外部jarにクラスパスを通して実行する方法を調査。

javax.tools.JavaCompilerのオプションで"-cp"または"-classpath"を指定する。例えば、以下のように書くことができる。

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
String[] options = new String[]{"-cp", "lib/mylib.jar"};
compiler.run(null, null, null, options, "MyClass.java");

これで、lib/mylib.jarに含まれるクラスをMyClass.javaで利用できる。

Hello JavaFX@JavaFX

Hello.java

package com.tanakahda;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class Hello extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setTitle("はじめてのJavaFX");
        FXMLLoader loader = new FXMLLoader(getClass().getResource("hello.fxml"));
        HBox root = loader.load();
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

hello.fxml

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<HBox>
    <children>
        <Label text="Hello world!" prefWidth="80.0" style="-fx-alignment:center"/>
    </children>
</HBox>

module-info.java

module JavaFXExamples {
    requires transitive javafx.controls;
    requires transitive javafx.fxml;
    opens com.tanakahda;
    exports com.tanakahda;
}

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>JavaFXExamples</groupId>
    <artifactId>JavaFXExamples</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <build>
        <sourceDirectory>src</sourceDirectory>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <release>17</release>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-controls</artifactId>
                <version>17</version>
        </dependency>
        <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-fxml</artifactId>
                <version>17</version>
        </dependency>
    </dependencies>
</project>