strlen と sizeof にまつわる不具合

以前に Mac 環境で C を使う場合、バイナリバイト列などのデータは NSData に任せた方がいいということは述べた。

いうまでもなく、C のデータの取り扱いの煩雑さは負担と考えているからだ。

そうはいっても、 既に(Objective ではない素の)C で各種データの定義が完了しているような場合、そういうわけにもいかない。例えば C で書かれた汎用ライブラリの機能に大きく依存しているようなプロジェクトではどうしてもどうなってしまう。

ここら辺、どの部分を NSData (というか cocoa の各種フレームワーク)に割り振り、どの部分を C で押し通すのかは、センスの出るところだと思う。

ところで、C 言語に関しては、巷の C 談義のようなものだけではうまくいかないと感じている。

ポインタの話は C を語る上で重要だが、ポインタだけ知っていても実用的なコードが書けるとは思えない。

ところで、この前、さるプログラムに不具合が出て、原因を調べていて気がついたのだが、以下のコードを実行した場合、結果はどうなると思います?

uint8_t AP1[] = {
        0x00, 0x20, 0x00, 0x88, 0x04
    };
printf("strlen(AP1): %d\n", strlen(AP1));
printf("sizeof(AP1): %d\n", sizeof(AP1));

この部分だけを取り出すとわかりやすいと思うが、結果は

strlen(AP1): 0
sizeof(AP1): 5

となる。

「終端文字 0x00 が先頭にあるのだから、文字列の長さを求める strlen では 0。使われているメモリのバイト数を求める sizeof では 5。当たり前じゃないか」と言われるかもしれないが、実際の(不具合のあった)コードはこんなにわかりやすくはない。

具体的には AP1 の末尾にバイト列を追加してバイト列を生成するためのメモリ確保の段階で

malloc(strlen(AP1) + strlen(hoge))

とやっていたんだな。

実は、これらコードを含むプログラムは USB を制御するコマンドで AP1[5] もそのコマンドの一つだった。

実際にデバイスを制御するわけだから、コマンドといえどもバイナリデータであってもおかしくはないのだが、そこら辺の事情をわからず「コマンド」と言われれば文字列を連想するのが普通ではなかろうか。

で、文字列と思いこんでこのような実装になってしまったと。

この手の不具合が発見しにくいのは、0 を含まない「コマンド」の場合は正しく動作してしまうからだ。

なお、バイト列とバイト列を結合させたい場合には

   uint8_t cmd[sizeof(AP1)+sizeof(hoge)];
    memcpy(cmd, AP1, sizeof(AP1));
    memcpy(cmd+sizeof(AP1), hoge, sizeof(hoge));

出力させる時は

for (int i = 0; i < sizeof(cmd); i++) {
            printf("%02x", cmd[i]);
    }

と書くとわかりやすいでしょうか。

 

chrome 拡張で PC ネイティブアプリとお話する -Native Messaging-

ネットを彷徨っていて「へー」といたく感心した記事。

Native Messaging を使い、ブラウザ拡張でプロセス間通信を行う方法

Native Messaging を用いると、Web ブラウザとは別に、ユーザーのPCにインストールされたネイティブアプリケーションとブラウザ拡張の間でメッセージ交換を行うことができます。つまり、Web ブラウザに用意された API ではアクセスできないハードウェアなどのリソースに対して、ブラウザ拡張からアクセスできるようになります。

こういう技術があるのは知っていたが、詳しい知識はゼロ。

拡張機能を持っている各種ブラウザなら適用可能な技術なようだが、chorome ユーザーが多いと思うので今回は chrome 拡張で考えてみる。

ところで・・

なぜ、Native Messaging が必要か

と思う人もいるかもしれない。

ワイもブラウザ関連の技術はそれほど詳しくないので上手い説明はできないが、簡単にいうなら「サイトページなどと連動して動く JavaScript は、基本的に PC 本体のリソースを使うことが制限されているから」ということになる。

というか、なんの制限もなく操作できたら困る。

昨今では、ネットを介して家電の操作を行う、なんてことが当たり前にできるようになったが、これがブラウザ経由で簡単にできてしまったら、帰宅時に家中めちゃくちゃなんて事態が生じてしまう。

家電ぐらいなら可愛いもので、USB を介して産業システムを制御しているような PC が悪意あるページから操作されてしまったら、社会レベルでかなり大変なことになる。

だが、その一方で、これだけネットが普及してしまうと、各種ネットサービスを提供するにあたってブラウザから PC のリソースを操作したいという要求は強く存在する。

実際、WebUSB のような規格も制定されつつあるのだが、各種機能が消えたり復活したりと仕様がなかなか一定しない。

それで Native Messaging のような技術が必要とされている、ということなんだろう。

最近だとマイナ関係のサイトで、この技術は結構使われているようだ。

今は過渡期なんでしょう。

話を元に戻そう。

まずは環境の構築。

準備

ネイティブアプリを呼び出す chrome 拡張とネイティブアプリの開発環境、それとサーバー機能のあるテスト環境が必要そうなので、以下のようなフォルダをまず作成。

opj-c フォルダで呼び出されるネイティブアプリの開発、
browser フォルダで chrome 拡張の開発、
testserver にテスト環境を整備、

という意図。

こうすれば、初心者でも混乱しないだろう。

testserver に関しては、今回は『イマドキのフロントエンド開発』で紹介されている node – webpack のものを使った。
なお、このときのエントリポイント index.js は空で構わない。

環境はバッチリなんだけどさ

環境を整えた後、ネット上のサンプルなどを試した・・・

のだが、見事なまでに動かないね(笑)。

これには理由があって

・現在の manifest.json のバージョンアップ(2 → 3)が 2024/11 と間近に迫っていて、現行の chrome は 3 推奨

・3 にすると書き方はけっこう変わるらしく従来の書き方ではエラーが出る

という背景がある。

とりあえず、「動く」chrome 拡張の manifest.json の書き方を提示しておくと

{
  "name": "Sample",
  "version": "1.0.0",
  "manifest_version": 3,
  "description": "Sample Chrome Extension",
  "background": {
    "service_worker": "background.js"
  },
  "permissions": [ "nativeMessaging"],
  "content_scripts": [{
    "matches": ["http://localhost:3000/*"],
    "js": [
      "content.js"
    ]
  }]
}

これを、browser フォルダに入れておく。

chrome 拡張に登録する際には、browser フォルダを指定し、デバッグなどは testserver 内で立ち上げた node 由来のサーバの指定のページで行うことができる。

ちなみに、2 → 3 で大きく変わったのは、 service_worker という書き方。

これに関しては google 公式が『拡張機能 Service Worker について』という記事を公開しているので、目を通しておきましょう。
また、Service Worker を使う API も公式がドキュメントを公開してくれている。

host 側アプリ

操作するホスト PC 側のアプリだが、何でもいいというものではなく、入出力は標準入出力に限定されている。(ここら辺も上述の「ブラウザから PC の操作は制限されている」という影響でてますね)

だから、メッセージのやり取りは stdio(C の場合)で行う必要がある。
Java であれば System.in あたりを使う。

適当に作成してみる。

もっとも、アプリは C や Java で作成した「いかにも」なものでなくてもよく、シェルスクリプトでもいいらしいのだが。
参照:『Google ChromeでトレイからCDを排出できるブラウザ拡張をつくりました(Native messaging版)

ホスト側アプリは、細かいことを気にしなければ、こんなものでも動く。

#include <stdio.h>
#include <limits.h>

int main(){
    
    uint8_t *input;
    uint8_t *buffer;
    if((input = (uint8_t *)fgets((char *)buffer,INT_MAX,stdin)) != NULL){
        //確認用
        //for(int i=0;i<sizeof(input);i++){
        //    printf("input data is %x", input[i]);
        //}
    }
    fwrite(input, sizeof(input), INT_MAX, stdout);
}

stdin で入ってきたバイト列をそのまま stdout で返しているだけ。

これでも chrome 拡張側(以下のコード)は反応してくれた。

ただ、仕様には全くあっていない。JavaScript は見事なまでに response を undefined と認識している(笑)。

ホストアプリからのメッセージは、正しくは

32byte ヘッダ(以下のデータ長) + base64 でエンコードされた JSON データ

のバイト列で返さないといけないらしい。
→と書いていたが、以下のコードのネイティブアプリからのレスポンス {“message”:”sign”} を Service Worker は正しく読んでいたので、必ずしも base64 でエンコードする必要はなさそうだ。

どうやら、ブラウザとホストアプリは Native Messaging を介してお話できているようですね。

もうちょっとちゃんと書くかと思っていたら、それ用のテストコードを書いている人がいたので、ちょっぴり改変して以下に掲げておく。(しかし、このコードはなかなか教育的ですね)

#include <stdio.h>
#include <limits.h>

#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>

uint8_t* read_input()
{
    uint32_t length;
    size_t count;
    int err;
    count = fread(&length, sizeof(uint32_t), 1, stdin);
    if (count != 1)
    {
        if (feof(stdin))
        {
            fprintf(stderr, "Unexpectedly encountered EOF while reading file\n");
        }
        else if ((err = ferror(stdin)))
        {
            fprintf(stderr, "An error occured while reading file, err code: %d\n", err);
            clearerr(stdin);
        }
        return NULL;
    }
    uint8_t* value = malloc((length + 1) * sizeof(*value));
    if (value == NULL)
    {
        fprintf(stderr, "An error occured while allocating memory for value");
        return NULL;
    }
    count = fread(value, sizeof(*value), length, stdin);
    if (count != length)
    {
        if (feof(stdin))
        {
            fprintf(stderr, "Unexpectedly encountered EOF while reading file\n");
        }
        else if ((err = ferror(stdin)))
        {
            fprintf(stderr, "An error occured while reading file, err code: %d\n", err);
            clearerr(stdin);
        }
        free(value);
        return NULL;
    }
    return value;
}

size_t write_output(const uint8_t* const value, uint32_t length)
{
    size_t count;
    int err;
    if (length > (1024 * 1024)) {
        fprintf(stderr, "Message too large");
        return 0;
    }
    count = fwrite(&length, sizeof(uint32_t), 1, stdout);
    if (count != 1)
    {
        if (feof(stdout))
        {
            fprintf(stderr, "Unexpectedly encountered EOF while reading file\n");
        }
        else if ((err = ferror(stdout)))
        {
            fprintf(stderr, "An error occured while reading file, err code: %d\n", err);
            clearerr(stdout);
        }
        return 0;
    }
    count = fwrite(value, sizeof(char), length, stdout);
    if (count != length)
    {
        if (feof(stdout))
        {
            fprintf(stderr, "Unexpectedly encountered EOF while writing file\n");
        }
        else if ((err = ferror(stdout)))
        {
            fprintf(stderr, "An error occured while writing file, err code: %d\n", err);
            clearerr(stdout);
        }
        return 0;
    }
    fflush(stdout);
    return length + 4;
}

int main()
{
    size_t count;
    uint8_t* val = read_input();
    if (val == NULL)
    {
        exit(EXIT_FAILURE);
    }
    sleep(3);//add akiba-chan
    count = write_output((uint8_t*)"{\"msg\":\"sign\"}", 14);
    if (count != 18)
    {
        free(val);
        exit(EXIT_FAILURE);
    }
    free(val);
}

「ちょっぴり改変」というのは、 main 関数内の sleep(3) の追加。
PC ハードのリソースを使う場合、大抵の場合、ソフト的な処理に比べ時間がかかる。
そのためリクエストを送ってからレスポンスが返ってくるまで秒単位の時間を設定できるようにした。
(x 秒待ちたいときは sleep(x) とする)

実際、マイナ(PKI)カードの署名処理では数秒単位の時間がかかる。

 

(続く)

Canonicalize XML in Java

はじめに

XML 署名の前に正規化 canonicalization を押さえておくべきだったな、とちょっと反省。

日頃使う html あたりを思い浮かべてもらえればピンとくると思うのだが、同じ内容を表現するのに元のコードは表記上はブレがある。
例えば、<tag> を <tag > と書こうが、ブラウザのパーサーは同じものと解釈する。
しかし、XML のデータ(構造)を丸ごと暗号化するような場合、これでは困る。
異なる数値データになってしまうからだ。
そこで、暗号化する前に何らかの仕方で表記のブレを統一しておきましょうという話になる。
これを正規化 canonicalization と呼んでいて、いくつかの方法が提案されている。

なお、よく C14N という表記が見られるが、これは anonicalizatio が14文字であることに起因する。

公開鍵基盤あたりの普及で電子データの長期保存のニーズは高まるだろうから、今後、この分野の知見は重要分野になってくるのは間違いない。

・・・とは思うのですが、この分野、ネット上の情報の質がちょっと・・・・。

例えば、この記事と同タイトルの英文記事があるのだが、そのサンプルがね。

普通に動かない(笑)。

説明はわかりやすいんすよ。

From the output, we can see that the Root node is removed in the canonicalized data, this is because the NodeFilter in NodeSetDataImpl has filtered this node. Next, the second my:Node has the xmlns:my node before Id node in the canonicalized form. This is based on the Canonical XML specification where the nodes should be in lexical order

出力から、正規化されたデータでルート ノードが削除されていることがわかります。これは、NodeSetDataImpl の NodeFilter がこのノードをフィルター処理したためです。次に、2 番目の my:Node には、正規化された形式で Id ノードの前に xmlns:my ノードがあります。これは、ノードが字句順に配置される必要がある Canonical XML 仕様に基づいています。

ここまで明快に具体的な正規化の方法を言及した記事はほとんどないっすから。
しかし、なんで(おそらくこの人の環境でしか動かない) tool なんてメソッドがソースに混入してるんだ???

車輪の再整備

よくわからない tool は、機能を読み取って変更。

オクテットストリームも InputStream でキャスト。

・・・

諸々する。

(ソースコード公開は少々お待ちを)

正規化例

修正して動かすと例えば、こんな結果が得られる。

何がどう変わったか?

<info/> → <info></info> あたりはわかりやすいでしょう。

よく見ると属性も変化してます。

要チェック。

その他

電子処方箋でも XML の正規化は必要になってきます。
このスレなど参照。

(続く)

Java XML 署名 API の使い方 -公式サンプル-

署名編の評判が良いようなので、次は検証(Validation)編。

と思ったのだが、オラクル公式サンプルが興味深かったので再掲。
レビューしてみる。

そのコードはこうだ。

import javax.xml.crypto.*;
import javax.xml.crypto.dsig.*;
import javax.xml.crypto.dom.*;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.keyinfo.*;
import java.io.FileInputStream;
import java.security.*;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

/**
 * This is a simple example of validating an XML 
 * Signature using the JSR 105 API. It assumes the key needed to
 * validate the signature is contained in a KeyValue KeyInfo. 
 */
public class Validate {

    //
    // Synopsis: java Validate [document]
    //
    //	  where "document" is the name of a file containing the XML document
    //	  to be validated.
    //
    public static void main(String[] args) throws Exception {

	// Instantiate the document to be validated
	DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	dbf.setNamespaceAware(true);
	Document doc =
            dbf.newDocumentBuilder().parse(new FileInputStream(args[0]));

	// Find Signature element
	NodeList nl = 
	    doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
	if (nl.getLength() == 0) {
	    throw new Exception("Cannot find Signature element");
	}

	// Create a DOM XMLSignatureFactory that will be used to unmarshal the 
	// document containing the XMLSignature 
	XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");

	// Create a DOMValidateContext and specify a KeyValue KeySelector
        // and document context
	DOMValidateContext valContext = new DOMValidateContext
	    (new KeyValueKeySelector(), nl.item(0));
	
	// unmarshal the XMLSignature
	XMLSignature signature = fac.unmarshalXMLSignature(valContext);

	// Validate the XMLSignature (generated above)
	boolean coreValidity = signature.validate(valContext); 

	// Check core validation status
	if (coreValidity == false) {
    	    System.err.println("Signature failed core validation"); 
	    boolean sv = signature.getSignatureValue().validate(valContext);
	    System.out.println("signature validation status: " + sv);
	    // check the validation status of each Reference
	    Iterator i = signature.getSignedInfo().getReferences().iterator();
	    for (int j=0; i.hasNext(); j++) {
		boolean refValid = 
		    ((Reference) i.next()).validate(valContext);
		System.out.println("ref["+j+"] validity status: " + refValid);
	    }
	} else {
    	    System.out.println("Signature passed core validation");
	}
    }

    /**
     * KeySelector which retrieves the public key out of the
     * KeyValue element and returns it.
     * NOTE: If the key algorithm doesn't match signature algorithm,
     * then the public key will be ignored.
     */
    private static class KeyValueKeySelector extends KeySelector {
	public KeySelectorResult select(KeyInfo keyInfo,
                                        KeySelector.Purpose purpose,
                                        AlgorithmMethod method,
                                        XMLCryptoContext context)
            throws KeySelectorException {
            if (keyInfo == null) {
		throw new KeySelectorException("Null KeyInfo object!");
            }
            SignatureMethod sm = (SignatureMethod) method;
            List list = keyInfo.getContent();

            for (int i = 0; i < list.size(); i++) {
		XMLStructure xmlStructure = (XMLStructure) list.get(i);
            	if (xmlStructure instanceof KeyValue) {
                    PublicKey pk = null;
                    try {
                        pk = ((KeyValue)xmlStructure).getPublicKey();
                    } catch (KeyException ke) {
                        throw new KeySelectorException(ke);
                    }
                    // make sure algorithm is compatible with method
                    if (algEquals(sm.getAlgorithm(), pk.getAlgorithm())) {
                        return new SimpleKeySelectorResult(pk);
                    }
		}
            }
            throw new KeySelectorException("No KeyValue element found!");
	}

        //@@@FIXME: this should also work for key types other than DSA/RSA
	static boolean algEquals(String algURI, String algName) {
            if (algName.equalsIgnoreCase("DSA") &&
		algURI.equalsIgnoreCase(SignatureMethod.DSA_SHA1)) {
		return true;
            } else if (algName.equalsIgnoreCase("RSA") &&
                       algURI.equalsIgnoreCase(SignatureMethod.RSA_SHA1)) {
		return true;
            } else {
		return false;
            }
	}
    }

    private static class SimpleKeySelectorResult implements KeySelectorResult {
	private PublicKey pk;
	SimpleKeySelectorResult(PublicKey pk) {
	    this.pk = pk;
	}

	public Key getKey() { return pk; }
    }
}

このコード自体はそんなにおかしくないと思うかもしれないが、対象となる xml ファイルも掲示されていてそれはこういうものだ。

<Envelope xmlns="urn:envelope">
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod xmlns="http://www.w3.org/2000/09/xmldsig#" Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/>
<SignatureMethod xmlns="http://www.w3.org/2000/09/xmldsig#" Algorithm="http://www.w3.org/2000/09/xmldsig#dsa-sha1"/>
<Reference xmlns="http://www.w3.org/2000/09/xmldsig#" URI="">
<Transforms xmlns="http://www.w3.org/2000/09/xmldsig#">
<Transform xmlns="http://www.w3.org/2000/09/xmldsig#" Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
</Transforms>
<DigestMethod xmlns="http://www.w3.org/2000/09/xmldsig#" Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue xmlns="http://www.w3.org/2000/09/xmldsig#">uooqbWYa5VCqcJCbuymBKqm17vY=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue xmlns="http://www.w3.org/2000/09/xmldsig#">eO7K1BdC0kzNvr1HpMf4hKoWsvl+oI04nMw55GO+Z5hyI6By3Oihow==</SignatureValue>
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<KeyValue xmlns="http://www.w3.org/2000/09/xmldsig#">
<DSAKeyValue xmlns="http://www.w3.org/2000/09/xmldsig#">
<P xmlns="http://www.w3.org/2000/09/xmldsig#">/KaCzo4Syrom78z3EQ5SbbB4sF7ey80etKII864WF64B81uRpH5t9jQTxeEu0ImbzRMqzVDZkVG9 xD7nN1kuFw==</P>
<Q xmlns="http://www.w3.org/2000/09/xmldsig#">li7dzDacuo67Jg7mtqEm2TRuOMU=</Q>
<G xmlns="http://www.w3.org/2000/09/xmldsig#">Z4Rxsnqc9E7pGknFFH2xqaryRPBaQ01khpMdLRQnG541Awtx/XPaF5Bpsy4pNWMOHCBiNU0Nogps QW5QvnlMpA==</G>
<Y xmlns="http://www.w3.org/2000/09/xmldsig#">OqFi0sGpvroi6Ut3m154QNWc6gavH3j2ZoRPDW7qVBbgk7XompuKvZe1owz0yvxq+1K+mWbL7ST+ t5nr6UFBCg==</Y>
</DSAKeyValue>
</KeyValue>
</KeyInfo>
</Signature>
</Envelope>

これを NetBeans あたりで実行すると(ちなみに Java17 あたりでもほとんど手直しなしで動く)エラーが出てきて、そのメッセージはなかなか興味深い。

Exception in thread "main" javax.xml.crypto.MarshalException: It is forbidden to use algorithm http://www.w3.org/2000/09/xmldsig#dsa-sha1 when secure validation is enabled

要するに dsa-sha1 が現在ではセキュリティ的に問題あるので使えませんよ、と警告してくれるわけだ。

また、Signature 要素を探すとき doc.getElementsByTagNameNS メソッドを使っていて、なんでだ?と訝しんでいたのだが、対象となる xml を眺めてなんか納得。