[インデックス 17447] ファイルの概要
このコミットは、Go言語の標準ライブラリ crypto/x509
パッケージにおけるX.509証明書の拡張機能の扱いを改善するものです。具体的には、パッケージが明示的にサポートしていない任意のX.509拡張を、証明書のパース時および生成時に扱えるようにする機能が追加されました。これにより、将来的に新しい拡張が導入された場合や、特定のカスタム拡張を扱う必要がある場合に、柔軟な対応が可能になります。
コミット
commit 87404c98871a1b912aaae93aea34561952fde0b8
Author: Adam Langley <agl@golang.org>
Date: Fri Aug 30 10:14:45 2013 -0400
crypto/x509: expose arbitary X.509 extensions.
This change allows people who want to parse or set odd X.509 extensions
to do so without having to add support for them all to the package.
I tried to make it so that only a single member: Extensions would be
needed. However, that would mean detecting when the caller had altered
the contents of it so that parsing and marshaling a certificate
wouldn't ignore all changes to the other members. This ended up being
messy, thus the current design where there are two members: one for
reading and another for writing.
As crypto/x509 adds support for more extensions in the future, the raw
extensions will still be in Extensions for older code that expects it
there. Also, future extensions will be overridden by any raw extensions
added to ExtraExtensions by code that was written before support was
added.
R=golang-dev, r
CC=golang-dev, jpsugar
https://golang.org/cl/12056043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/87404c98871a1b912aaae93aea34561952fde0b8
元コミット内容
このコミットは、crypto/x509
パッケージにおいて、任意のX.509拡張を公開することを目的としています。これにより、パッケージが明示的にサポートしていない「奇妙な」X.509拡張をパースしたり設定したりすることが、パッケージにそれらすべてのサポートを追加することなく可能になります。
当初は Extensions
という単一のメンバーで読み書きの両方を賄おうとしましたが、呼び出し元がその内容を変更した際に、証明書のパースとマーシャリングが他のメンバーへの変更を無視しないように検出することが複雑になるため、このアプローチは断念されました。
そのため、現在の設計では、読み取り用の Extensions
と書き込み用の ExtraExtensions
という2つのメンバーが導入されています。
将来的に crypto/x509
がより多くの拡張をサポートするようになった場合でも、古いコードが Extensions
フィールドに生の拡張を期待しているため、そこには引き続き生の拡張が含まれます。また、将来の拡張は、サポートが追加される前に書かれたコードによって ExtraExtensions
に追加された生の拡張によって上書きされるようになります。
変更の背景
X.509証明書は、その基本的な情報(公開鍵、発行者、有効期間など)に加えて、様々な追加情報やポリシーを「拡張」として含めることができます。これらの拡張は、証明書の用途を定義したり、追加の制約を課したりするために使用されます。しかし、X.509の仕様は非常に広範であり、すべての可能な拡張をGoの crypto/x509
パッケージが明示的にサポートすることは現実的ではありません。
これまでの crypto/x509
パッケージでは、特定の既知の拡張のみが Certificate
構造体の個別のフィールドとしてパースされ、アクセス可能でした。このため、パッケージがサポートしていない新しい、あるいはカスタムのX.509拡張を含む証明書を扱う場合、開発者はこれらの拡張にアクセスしたり、独自の拡張を持つ証明書を生成したりすることが困難でした。
このコミットの背景には、以下のような課題がありました。
- 未知の拡張への対応の欠如:
crypto/x509
が明示的にパースしない拡張は、事実上無視されていました。これにより、特定のアプリケーションで必要とされるカスタム拡張や、新しい標準で導入された拡張を扱うことができませんでした。 - 柔軟性の不足: 証明書を生成する際に、標準でサポートされていない拡張を組み込むことができませんでした。これは、特定のセキュリティ要件やプロトコルに準拠した証明書を作成する上で制約となっていました。
- 将来性への対応: X.509の拡張は時間とともに進化し、新しいものが追加される可能性があります。パッケージがすべての拡張を明示的にサポートするのを待つことなく、これらの新しい拡張に早期に対応できるメカニズムが必要でした。
このコミットは、これらの課題に対処し、crypto/x509
パッケージの柔軟性と将来性を高めることを目的としています。
前提知識の解説
このコミットの変更内容を理解するためには、以下の前提知識が役立ちます。
X.509 証明書
X.509は、公開鍵証明書の標準フォーマットを定義するITU-Tの標準です。主に公開鍵基盤(PKI)において、エンティティ(ユーザー、サーバーなど)の公開鍵とそのエンティティの身元を結びつけるために使用されます。X.509証明書には、以下のような主要な情報が含まれます。
- バージョン: 証明書のバージョン(例: v3)。
- シリアル番号: 証明書を一意に識別する番号。
- 署名アルゴリズム: 証明書の署名に使用されたアルゴリズム。
- 発行者: 証明書を発行した認証局(CA)の名前。
- 有効期間: 証明書が有効である期間(開始日時と終了日時)。
- サブジェクト: 証明書が発行されたエンティティの名前。
- 公開鍵情報: サブジェクトの公開鍵とそのアルゴリズム。
- 発行者の一意な識別子(オプション): 発行者を一意に識別するための情報。
- サブジェクトの一意な識別子(オプション): サブジェクトを一意に識別するための情報。
- 拡張: 証明書に追加の属性や制約を付与するためのフィールド。
X.509 拡張 (Extensions)
X.509 v3から導入された「拡張」は、証明書に標準的なフィールドでは表現できない追加情報を含めるためのメカニズムです。各拡張は、以下の要素で構成されます。
- OID (Object Identifier): 拡張の種類を一意に識別する数値のシーケンス。例えば、
2.5.29.15
は Key Usage 拡張を表します。 - Criticality (Critical): 拡張がクリティカル(必須)であるか、非クリティカル(オプション)であるかを示すブール値。クリティカルな拡張は、証明書を処理するアプリケーションがその拡張を理解し、適切に処理できなければ、証明書を拒否しなければなりません。非クリティカルな拡張は、理解できなくても証明書の処理を続行できます。
- Value: 拡張の実際のデータ。このデータはASN.1(Abstract Syntax Notation One)でエンコードされます。
一般的なX.509拡張の例としては、以下のようなものがあります。
- Key Usage: 公開鍵の用途(例: デジタル署名、鍵の暗号化、証明書署名など)を定義します。
- Extended Key Usage: Key Usageを補完し、特定のアプリケーションでの鍵の用途(例: サーバー認証、クライアント認証、コード署名など)を定義します。
- Subject Alternative Name (SAN): サブジェクトの代替名(DNS名、IPアドレス、メールアドレスなど)を提供します。これにより、単一の証明書で複数のホスト名やサービスをカバーできます。
- Basic Constraints: 証明書がCA証明書であるか否か、およびCA証明書の場合のパス長制約を定義します。
- Authority Key Identifier / Subject Key Identifier: 鍵のペアを一意に識別するために使用されます。
ASN.1 (Abstract Syntax Notation One)
ASN.1は、データ構造を記述するための標準的な記法です。X.509証明書を含む多くの暗号化プロトコルや通信プロトコルで、データのエンコーディング(バイト列への変換)とデコーディング(バイト列からの復元)に使用されます。ASN.1は、データ型(整数、文字列、シーケンス、セットなど)と、それらをバイト列に変換するためのエンコーディングルール(例: DER - Distinguished Encoding Rules)を定義します。
pkix.Extension
構造体の Value
フィールドは、ASN.1でエンコードされた拡張の生データを含んでいます。
Go言語の crypto/x509
パッケージ
Go言語の crypto/x509
パッケージは、X.509証明書のパース、検証、生成を行うための機能を提供します。これまでのバージョンでは、Certificate
構造体には、KeyUsage
, ExtKeyUsage
, DNSNames
など、特定の既知の拡張に対応するフィールドが直接定義されていました。このコミットは、これらの明示的なフィールドに加えて、任意の拡張を扱うための汎用的なメカニズムを導入します。
技術的詳細
このコミットの技術的な核心は、crypto/x509.Certificate
構造体に2つの新しいフィールド Extensions
と ExtraExtensions
を導入し、証明書のパースとマーシャリング(DERエンコード)のロジックをこれらに対応するように変更した点にあります。
Certificate
構造体へのフィールド追加
-
Extensions []pkix.Extension
:- このフィールドは、証明書をパースする際に、証明書に含まれるすべての生のX.509拡張(
pkix.Extension
型)を格納するために使用されます。 crypto/x509
パッケージが明示的にパースしてCertificate
構造体の他のフィールド(例:KeyUsage
,DNSNames
)にマッピングする拡張も、このExtensions
スライスに含まれます。- このフィールドは読み取り専用であり、証明書をマーシャリングする際には無視されます。これは、古いコードが
Extensions
フィールドに生の拡張が存在することを期待する場合に、後方互換性を提供するためです。
- このフィールドは、証明書をパースする際に、証明書に含まれるすべての生のX.509拡張(
-
ExtraExtensions []pkix.Extension
:- このフィールドは、証明書をマーシャリングする際に、追加で含めたい生のX.509拡張を格納するために使用されます。
- このフィールドに含まれる拡張は、
Certificate
構造体の他のフィールド(例:KeyUsage
,DNSNames
)から自動的に生成される拡張よりも優先されます。つまり、ExtraExtensions
に同じOIDを持つ拡張が含まれている場合、自動生成される拡張は追加されません。 - このフィールドは書き込み専用であり、証明書をパースする際には設定されません。
この「読み取り用」と「書き込み用」の2つのフィールドに分ける設計は、コミットメッセージで述べられているように、単一のフィールドで両方を賄おうとした際の複雑さを回避するためのものです。単一フィールドの場合、ユーザーがそのフィールドの内容を変更した際に、それが他の Certificate
メンバーの変更とどのように相互作用するかを検出するのが困難でした。
パースロジックの変更 (parseCertificate
関数)
parseCertificate
関数は、入力されたDERエンコードされた証明書を Certificate
構造体に変換する役割を担います。このコミットでは、この関数に以下の変更が加えられました。
- 証明書に含まれるすべての拡張(
in.TBSCertificate.Extensions
)がループ処理され、それぞれがout.Extensions
スライスに追加されるようになりました。これにより、パッケージが明示的にサポートしていない拡張であっても、生の形式でアクセスできるようになります。
マーシャリングロジックの変更 (buildExtensions
関数)
buildExtensions
関数は、Certificate
構造体の内容に基づいて、証明書に含めるべきX.509拡張のリストを構築する役割を担います。このコミットでは、この関数に以下の重要な変更が加えられました。
-
oidInExtensions
ヘルパー関数の導入:- この新しい関数は、特定のOIDが
pkix.Extension
のスライス内に存在するかどうかを効率的にチェックします。 - これは、
ExtraExtensions
に含まれる拡張が、Certificate
構造体の他のフィールドから自動的に生成される拡張と重複しないようにするために使用されます。
- この新しい関数は、特定のOIDが
-
自動生成される拡張の抑制:
buildExtensions
関数内で、KeyUsage
,ExtKeyUsage
,BasicConstraints
,SubjectKeyId
,AuthorityKeyId
,AuthorityInfoAccess
,SubjectAltName
,CertificatePolicies
,NameConstraints
,CRLDistributionPoints
など、Certificate
構造体の既存のフィールドから自動的に生成される各拡張について、ExtraExtensions
に同じOIDの拡張が既に存在しないかどうかのチェックが追加されました。- 例えば、
KeyUsage
拡張を生成する条件はif template.KeyUsage != 0
からif template.KeyUsage != 0 && !oidInExtensions(oidExtensionKeyUsage, template.ExtraExtensions)
に変更されました。これにより、ユーザーがExtraExtensions
を通じてKeyUsage
を明示的に指定した場合、パッケージが自動的に生成するKeyUsage
拡張は追加されなくなります。これは、ユーザーが提供する生の拡張が優先されることを保証します。
-
ExtraExtensions
の追加:buildExtensions
関数の最後に、構築された拡張のリストにtemplate.ExtraExtensions
の内容がappend
されるようになりました。return append(ret[:n], template.ExtraExtensions...), nil
という変更により、ExtraExtensions
に含まれるすべての拡張が、自動生成された拡張の後に(ただし、自動生成が抑制された場合はその代わりに)証明書に組み込まれます。
これらの変更により、開発者は ExtraExtensions
を使用して、Goの crypto/x509
パッケージが明示的にサポートしていない任意のX.509拡張を証明書に含めることができるようになり、同時に、既存のフィールドから自動生成される拡張との競合を避けることができます。
コアとなるコードの変更箇所
src/pkg/crypto/x509/x509.go
-
Certificate
構造体へのフィールド追加:type Certificate struct { // ... 既存のフィールド ... // Extensions contains raw X.509 extensions. When parsing certificates, // this can be used to extract non-critical extensions that are not // parsed by this package. When marshaling certificates, the Extensions // field is ignored, see ExtraExtensions. Extensions []pkix.Extension // ExtraExtensions contains extensions to be copied, raw, into any // marshaled certificates. Values override any extensions that would // otherwise be produced based on the other fields. The ExtraExtensions // field is not populated when parsing certificates, see Extensions. ExtraExtensions []pkix.Extension // ... 既存のフィールド ... }
-
parseCertificate
関数でのExtensions
フィールドのポピュレート:func parseCertificate(in *certificate) (*Certificate, error) { // ... 既存のパースロジック ... for _, e := range in.TBSCertificate.Extensions { out.Extensions = append(out.Extensions, e) // 追加行 // ... 既存の拡張パースロジック ... } // ... }
-
oidInExtensions
ヘルパー関数の追加:// oidInExtensions returns whether an extension with the given oid exists in // extensions. func oidInExtensions(oid asn1.ObjectIdentifier, extensions []pkix.Extension) bool { for _, e := range extensions { if e.Id.Equal(oid) { return true } } return false }
-
buildExtensions
関数でのExtraExtensions
の考慮と追加:各拡張の生成ロジックに
!oidInExtensions(oidExtension..., template.ExtraExtensions)
の条件が追加されました。例:// KeyUsage - if template.KeyUsage != 0 { + if template.KeyUsage != 0 && + !oidInExtensions(oidExtensionKeyUsage, template.ExtraExtensions) { ret[n].Id = oidExtensionKeyUsage ret[n].Critical = true // ... } // Extended Key Usage - if len(template.ExtKeyUsage) > 0 || len(template.UnknownExtKeyUsage) > 0 { + if (len(template.ExtKeyUsage) > 0 || len(template.UnknownExtKeyUsage) > 0) && + !oidInExtensions(oidExtensionExtendedKeyUsage, template.ExtraExtensions) { ret[n].Id = oidExtensionExtendedKeyUsage // ... } // Basic Constraints - if template.BasicConstraintsValid { + if template.BasicConstraintsValid && !oidInExtensions(oidExtensionBasicConstraints, template.ExtraExtensions) { ret[n].Id = oidExtensionBasicConstraints // ... } // Subject Key ID - if len(template.SubjectKeyId) > 0 { + if len(template.SubjectKeyId) > 0 && !oidInExtensions(oidExtensionSubjectKeyId, template.ExtraExtensions) { ret[n].Id = oidExtensionSubjectKeyId // ... } // Authority Key ID - if len(template.AuthorityKeyId) > 0 { + if len(template.AuthorityKeyId) > 0 && !oidInExtensions(oidExtensionAuthorityKeyId, template.ExtraExtensions) { ret[n].Id = oidExtensionAuthorityKeyId // ... } // Authority Information Access - if len(template.OCSPServer) > 0 || len(template.IssuingCertificateURL) > 0 { + if (len(template.OCSPServer) > 0 || len(template.IssuingCertificateURL) > 0) && + !oidInExtensions(oidExtensionAuthorityInfoAccess, template.ExtraExtensions) { ret[n].Id = oidExtensionAuthorityInfoAccess // ... } // Subject Alternative Name - if len(template.DNSNames) > 0 || len(template.EmailAddresses) > 0 || len(template.IPAddresses) > 0 { + if (len(template.DNSNames) > 0 || len(template.EmailAddresses) > 0 || len(template.IPAddresses) > 0) && + !oidInExtensions(oidExtensionSubjectAltName, template.ExtraExtensions) { ret[n].Id = oidExtensionSubjectAltName // ... } // Certificate Policies - if len(template.PolicyIdentifiers) > 0 { + if len(template.PolicyIdentifiers) > 0 && + !oidInExtensions(oidExtensionCertificatePolicies, template.ExtraExtensions) { ret[n].Id = oidExtensionCertificatePolicies // ... } // Name Constraints - if len(template.PermittedDNSDomains) > 0 { + if len(template.PermittedDNSDomains) > 0 && + !oidInExtensions(oidExtensionNameConstraints, template.ExtraExtensions) { ret[n].Id = oidExtensionNameConstraints // ... } // CRL Distribution Points - if len(template.CRLDistributionPoints) > 0 { + if len(template.CRLDistributionPoints) > 0 && + !oidInExtensions(oidExtensionCRLDistributionPoints, template.ExtraExtensions) { ret[n].Id = oidExtensionCRLDistributionPoints // ... }
そして、関数の最後に
ExtraExtensions
を追加する行が変更されました。- return ret[0:n], nil + return append(ret[:n], template.ExtraExtensions...), nil
src/pkg/crypto/x509/x509_test.go
-
TestCertificateParse
でのExtensions
フィールドのテスト:func TestCertificateParse(t *testing.T) { // ... 既存のテストコード ... const expectedExtensions = 4 if n := len(certs[0].Extensions); n != expectedExtensions { t.Errorf("want %d extensions, got %d", expectedExtensions, n) } }
-
TestCreateSelfSignedCertificate
でのExtraExtensions
のテスト:template
構造体にExtraExtensions
を追加し、その動作を検証するテストが追加されました。func TestCreateSelfSignedCertificate(t *testing.T) { // ... 既存のテストコード ... testExtKeyUsage := []ExtKeyUsage{ExtKeyUsageClientAuth, ExtKeyUsageServerAuth} testUnknownExtKeyUsage := []asn1.ObjectIdentifier{[]int{1, 2, 3}, []int{2, 59, 1}} extraExtensionData := []byte("extra extension") // 追加行 // ... template := Certificate{ // ... 既存のテンプレート設定 ... ExtraExtensions: []pkix.Extension{ // 追加されたExtraExtensions { Id: []int{1, 2, 3, 4}, Value: extraExtensionData, }, // This extension should override the SubjectKeyId, above. { Id: oidExtensionSubjectKeyId, Critical: false, Value: []byte{0x04, 0x04, 4, 3, 2, 1}, }, }, } // ... if !bytes.Equal(cert.SubjectKeyId, []byte{4, 3, 2, 1}) { // ExtraExtensionsによるSubjectKeyIdの上書き検証 t.Errorf("%s: ExtraExtensions didn't override SubjectKeyId", test.name) } if bytes.Index(derBytes, extraExtensionData) == -1 { // カスタム拡張データの存在検証 t.Errorf("%s: didn't find extra extension in DER output", test.name) } // ... }
コアとなるコードの解説
このコミットのコアとなる変更は、crypto/x509.Certificate
構造体に Extensions
と ExtraExtensions
という2つの新しいスライスフィールドを導入し、それらを証明書のパースとマーシャリングのライフサイクルに統合した点です。
Certificate
構造体の拡張
-
Extensions []pkix.Extension
:- これは、証明書を読み込む(パースする)際に、証明書に含まれるすべてのX.509拡張の「生」の表現を保持するためのフィールドです。
parseCertificate
関数内で、証明書から読み取られた各拡張がこのスライスに追加されます。これにより、crypto/x509
パッケージが明示的にサポートしていない未知の拡張であっても、開発者はこのExtensions
スライスを通じてそのOIDと生の値にアクセスできるようになります。- このフィールドは、証明書をDERエンコードする際には使用されません。これは、パースされた証明書を再エンコードする際に、元の証明書に含まれていたがパッケージが理解しない拡張が失われるのを防ぐためです。
-
ExtraExtensions []pkix.Extension
:- これは、新しい証明書を作成する(マーシャリングする)際に、開発者が明示的に含めたい追加のX.509拡張を指定するためのフィールドです。
buildExtensions
関数内で、このExtraExtensions
スライスに含まれる拡張が、Certificate
構造体の他のフィールド(例:KeyUsage
,DNSNames
)から自動的に生成される拡張よりも優先されます。- 具体的には、
buildExtensions
関数は、各標準拡張を生成する前に、ExtraExtensions
に同じOIDの拡張が既に存在しないかをoidInExtensions
ヘルパー関数を使ってチェックします。もし存在すれば、標準拡張の自動生成はスキップされ、ExtraExtensions
に指定された値が使用されます。 - 最終的に、
buildExtensions
関数は、自動生成された(または抑制されなかった)拡張のリストの末尾にExtraExtensions
の内容を追加して返します。これにより、開発者が指定したカスタム拡張が証明書に確実に含まれるようになります。
oidInExtensions
ヘルパー関数
この小さなユーティリティ関数は、pkix.Extension
のスライスを反復処理し、指定された asn1.ObjectIdentifier
(OID) と一致する拡張が存在するかどうかを効率的に判断します。これは、ExtraExtensions
に含まれるカスタム拡張が、crypto/x509
パッケージが自動的に生成する標準拡張と重複するのを防ぐための重要な役割を果たします。
パースとマーシャリングのフロー
-
パース時:
parseCertificate
関数が呼び出されると、入力された証明書のすべての拡張が読み取られます。- これらの拡張は、
Certificate
構造体のExtensions
フィールドにそのまま追加されます。 - 同時に、パッケージが認識する既知の拡張は、
Certificate
構造体の対応するフィールド(例:KeyUsage
)にもパースされて格納されます。
-
マーシャリング時:
CreateCertificate
などの関数が呼び出され、buildExtensions
関数が内部的に呼び出されます。buildExtensions
は、まずCertificate
構造体の既存のフィールド(KeyUsage
,DNSNames
など)に基づいて標準的な拡張を構築しようとします。- この際、各標準拡張について、
ExtraExtensions
に同じOIDの拡張が既に存在しないかを確認します。存在する場合は、その標準拡張の自動生成をスキップします。 - すべての標準拡張の処理が終わった後、
ExtraExtensions
に含まれるすべての拡張が、構築された拡張リストの末尾に追加されます。 - 最終的な拡張リストが証明書に組み込まれ、DERエンコードされます。
この設計により、crypto/x509
パッケージは、既知の標準拡張を扱いながらも、未知のカスタム拡張に対する柔軟な読み書きの機能を提供できるようになりました。これは、X.509証明書を扱うアプリケーションの多様なニーズに対応するための重要な改善です。
関連リンク
- Go言語
crypto/x509
パッケージのドキュメント: https://pkg.go.dev/crypto/x509 - X.509 証明書に関するRFC:
- RFC 5280 (Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile): https://datatracker.ietf.org/doc/html/rfc5280
- ASN.1 の概要: https://www.itu.int/en/ITU-T/asn1/Pages/default.aspx
参考にした情報源リンク
- Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語のコードレビューシステム (Gerrit): https://go.dev/cl/ (コミットメッセージに記載されている
https://golang.org/cl/12056043
は、このGerritの変更リストへのリンクです。) - X.509 Extensions (Wikipedia): https://en.wikipedia.org/wiki/X.509#Extensions
- Object Identifier (Wikipedia): https://en.wikipedia.org/wiki/Object_identifier
- Abstract Syntax Notation One (Wikipedia): https://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One
- Go言語の
pkix
パッケージのドキュメント: https://pkg.go.dev/crypto/x509/pkix