Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 15269] ファイルの概要

このコミットは、Go言語のcrypto/x509パッケージにおいて、X.509証明書のSubject Alternative Name (SAN) にIPアドレスを含める機能(IP SANs)をサポートするための変更を導入しています。具体的には、IPアドレス形式のSANのマーシャリング(エンコード)、アンマーシャリング(デコード)、および検証の機能が追加されました。また、RFC 6125に準拠し、IPアドレスはIP SANsに対してのみ検証されるようになり、以前のようにホスト名に対して検証されることはなくなりました。

コミット

commit 5b20a18f3b985cdf116a0ea6dd589d26056b16ad
Author: Adam Langley <agl@golang.org>
Date:   Fri Feb 15 10:40:17 2013 -0500

    crypto/x509: support IP SANs.
    
    Subject Alternative Names in X.509 certificates may include IP
    addresses. This change adds support for marshaling, unmarshaling and
    verifying this form of SAN.
    
    It also causes IP addresses to only be checked against IP SANs,
    rather than against hostnames as was previously the case. This
    reflects RFC 6125.
    
    Fixes #4658.
    
    R=golang-dev, mikioh.mikioh, bradfitz
    CC=golang-dev
    https://golang.org/cl/7336046

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/5b20a18f3b985cdf116a0ea6dd589d26056b16ad

元コミット内容

crypto/x509: support IP SANs.

Subject Alternative Names in X.509 certificates may include IP
addresses. This change adds support for marshaling, unmarshaling and
verifying this form of SAN.

It also causes IP addresses to only be checked against IP SANs,
rather than against hostnames as was previously the case. This
reflects RFC 6125.

Fixes #4658.

R=golang-dev, mikioh.mikioh, bradfitz
CC=golang-dev
https://golang.org/cl/7336046

変更の背景

X.509証明書は、ウェブサイトやその他のエンティティの身元を検証するために広く使用されるデジタル証明書です。これらの証明書は、通常、Common Name (CN)フィールドにホスト名を含みますが、より柔軟な識別子をサポートするためにSubject Alternative Name (SAN)拡張が導入されました。SANは、DNS名、URI、電子メールアドレスなど、さまざまな形式の識別子を格納できます。

このコミット以前のGoのcrypto/x509パッケージでは、IPアドレスがSANとして適切に扱われていませんでした。特に、IPアドレスを検証する際に、証明書内のDNS名(DNSNamesフィールド)やCommon Nameに対して照合しようとする問題がありました。これは、IPアドレスとホスト名が異なる種類の識別子であり、セキュリティ上の懸念や誤った検証結果につながる可能性がありました。

この問題に対処するため、RFC 6125「Representation and Verification of Domain Names in the Internet Public Key Infrastructure」が重要な指針となります。RFC 6125は、ドメイン名(およびIPアドレス)がX.509証明書に対してどのように検証されるべきかについて明確なルールを定めています。このRFCの重要な点は、IPアドレスはIPアドレスとしてSANに記述され、IPアドレスの検証はIP SANに対してのみ行われるべきであるという点です。

このコミットは、Goのcrypto/x509パッケージをRFC 6125に準拠させ、IPアドレスのSANサポートを完全に実装することで、より堅牢で安全な証明書検証メカニズムを提供することを目的としています。これにより、IPアドレスを直接指定してアクセスするサービスや、IPアドレスベースの仮想ホスティングなどにおいて、証明書の検証が正しく行われるようになります。

前提知識の解説

X.509 証明書

X.509は、公開鍵証明書の標準フォーマットを定義するITU-Tの標準です。インターネット上で広く使用されており、TLS/SSL通信においてサーバーやクライアントの身元を証明するために利用されます。X.509証明書には、公開鍵、所有者の識別情報(Common Name、Organizationなど)、発行者の情報、有効期間、署名などが含まれます。

Subject Alternative Name (SAN)

Subject Alternative Name (SAN) は、X.509証明書の拡張フィールドの一つです。これは、証明書が有効である追加の識別子を指定するために使用されます。従来の証明書ではCommon Name (CN)フィールドが主要な識別子でしたが、CNは単一のホスト名しか指定できませんでした。SANは、以下のような複数の種類の識別子をサポートします。

  • DNS Name (dNSName): ホスト名(例: www.example.com)。ワイルドカード(例: *.example.com)も使用できます。
  • IP Address (iPAddress): IPアドレス(IPv4またはIPv6)。
  • Email Address (rfc822Name): 電子メールアドレス。
  • URI (uniformResourceIdentifier): Uniform Resource Identifier。
  • Directory Name (directoryName): X.500形式の識別名。
  • Registered ID (registeredID): 登録されたオブジェクト識別子。

SANは、単一の証明書で複数のドメイン名やIPアドレスをカバーできるため、特にバーチャルホスティング環境や、複数のサービスが同じ証明書を共有する場合に非常に有用です。

IP SANs

IP SANsは、SAN拡張の中でも特にIPアドレスを指定する形式です。これにより、証明書が特定のIPアドレスに対して有効であることを明示的に示すことができます。例えば、https://192.168.1.1のようなIPアドレスで直接アクセスされる内部サービスや、ロードバランサーのIPアドレスに対して証明書を発行する場合に利用されます。

RFC 6125

RFC 6125は、「Representation and Verification of Domain Names in the Internet Public Key Infrastructure」というタイトルのIETF標準です。このRFCは、TLS/SSLなどのプロトコルにおいて、X.509証明書内の識別子(特にドメイン名)がどのように表現され、検証されるべきかについて詳細なガイドラインを提供します。

RFC 6125の重要なポイントの一つは、証明書の検証において、クライアントが接続しようとしている識別子(ホスト名またはIPアドレス)と証明書内の識別子との照合ルールを明確にすることです。特にIPアドレスの検証に関して、RFC 6125は以下の点を強調しています。

  • IPアドレスはIP SANsに対してのみ検証されるべきである。 証明書にIPアドレスがSANとして含まれている場合、そのIPアドレスはIP SANフィールドと照合されるべきです。
  • IPアドレスをDNS名やCommon Nameとして扱うべきではない。 IPアドレスをDNS名としてSANに含めたり、Common Nameとして使用したりすることは推奨されません。また、IPアドレスを検証する際に、DNS名やCommon Nameフィールドと照合することは誤りであるとされています。

このコミットは、Goのcrypto/x509パッケージがこのRFC 6125の原則に準拠するように変更を加えるものです。

マーシャリングとアンマーシャリング

  • マーシャリング (Marshaling): プログラム内のデータ構造(この場合はGoのx509.Certificate構造体)を、外部形式(この場合はX.509証明書のDERエンコードされたバイナリ形式)に変換するプロセスです。これにより、データはファイルに保存されたり、ネットワーク経由で送信されたりすることができます。
  • アンマーシャリング (Unmarshaling): 外部形式のデータを、プログラム内のデータ構造に変換し直すプロセスです。これにより、受信した証明書データをGoのプログラムで扱えるようになります。

X.509証明書はASN.1 (Abstract Syntax Notation One) とDER (Distinguished Encoding Rules) を用いてエンコードされます。SAN拡張内の各識別子(DNS名、IPアドレスなど)は、ASN.1のGeneralName構造体として表現され、それぞれ異なるタグ(例: DNS名はタグ2、IPアドレスはタグ7)を持ちます。

技術的詳細

このコミットは、Goのcrypto/x509パッケージとその関連ツールにおいて、IP SANsのサポートを全面的に統合するために複数のファイルにわたる変更を加えています。

src/pkg/crypto/x509/x509.go

このファイルは、X.509証明書のコア構造と、証明書のパース(アンマーシャリング)および構築(マーシャリング)ロジックを定義しています。

  1. Certificate構造体へのIPAddressesフィールドの追加: Certificate構造体にIPAddresses []net.IPという新しいフィールドが追加されました。これにより、パースされた証明書からIP SANsを直接取得できるようになります。

    type Certificate struct {
        // ...
        // Subject Alternate Name values
        DNSNames       []string
        EmailAddresses []string
        IPAddresses    []net.IP // 新しく追加されたフィールド
        // ...
    }
    
  2. parseCertificate関数でのIP SANsのパース: parseCertificate関数は、DERエンコードされた証明書をパースし、Certificate構造体にデータを格納します。この関数内で、SAN拡張のGeneralNameを処理するロジックが変更されました。 ASN.1のGeneralName構造体において、IPアドレスはtag 7としてエンコードされます。この変更により、tag 7を持つRawValueが検出された場合、そのバイト列がnet.IPに変換され、Certificate.IPAddressesスライスに追加されるようになりました。IPv4とIPv6の両方に対応し、不正な長さのIPアドレスが検出された場合はエラーを返します。

    // parseCertificate関数内
    case 7: // iPAddress
        switch len(v.Bytes) {
        case net.IPv4len, net.IPv6len:
            out.IPAddresses = append(out.IPAddresses, v.Bytes)
        default:
            return nil, errors.New("x509: certificate contained IP address of length " + strconv.Itoa(len(v.Bytes)))
        }
    
  3. buildExtensions関数でのIP SANsのエンコード: buildExtensions関数は、Certificate構造体からX.509証明書の拡張(SANを含む)を構築し、DERエンコードします。この関数も変更され、Certificate.IPAddressesフィールドにIPアドレスが存在する場合、それらがASN.1のtag 7として適切にエンコードされ、SAN拡張に追加されるようになりました。IPv4アドレスは、可能であれば4バイト形式でエンコードされます。

    // buildExtensions関数内
    if len(template.DNSNames) > 0 || len(template.EmailAddresses) > 0 || len(template.IPAddresses) > 0 {
        ret[n].Id = oidExtensionSubjectAltName
        var rawValues []asn1.RawValue
        // ... (DNSNamesとEmailAddressesの処理)
        for _, rawIP := range template.IPAddresses {
            // If possible, we always want to encode IPv4 addresses in 4 bytes.
            ip := rawIP.To4()
            if ip == nil {
                ip = rawIP
            }
            rawValues = append(rawValues, asn1.RawValue{Tag: 7, Class: 2, Bytes: ip})
        }
        ret[n].Value, err = asn1.Marshal(rawValues)
        // ...
    }
    

src/pkg/crypto/x509/verify.go

このファイルは、X.509証明書の検証ロジック、特にホスト名検証を担当しています。

  1. VerifyHostname関数の変更: VerifyHostname関数は、与えられたホスト名が証明書によって有効であるかを検証します。この関数は、RFC 6125の原則に従い、IPアドレスの検証ロジックが大幅に変更されました。

    • 検証対象のホスト名がIPアドレス(IPv4またはIPv6、[]で囲まれた形式も含む)である場合、証明書のIPAddressesフィールド(IP SANs)のみが照合対象となります。
    • IP SANsの中に一致するIPアドレスが見つかれば検証は成功します。
    • IPアドレスであるにもかかわらず、証明書にIP SANsが含まれていない場合、または一致するIP SANsがない場合は、HostnameErrorが返されます。
    • これにより、IPアドレスがDNS名やCommon Nameと照合されるという以前の誤った挙動が修正されました。
    // VerifyHostname関数内
    candidateIP := h
    if len(h) >= 3 && h[0] == '[' && h[len(h)-1] == ']' {
        candidateIP = h[1 : len(h)-1]
    }
    if ip := net.ParseIP(candidateIP); ip != nil {
        // We only match IP addresses against IP SANs.
        // https://tools.ietf.org/html/rfc6125#appendix-B.2
        for _, candidate := range c.IPAddresses {
            if ip.Equal(candidate) {
                return nil
            }
        }
        return HostnameError{c, candidateIP}
    }
    // ... (IPアドレスでない場合は既存のDNSName/CommonName検証ロジック)
    
  2. HostnameErrorの変更: HostnameError構造体のError()メソッドが、IPアドレス検証の失敗時に、より適切なエラーメッセージを生成するように変更されました。IPアドレスがIP SANsに対して検証されなかった場合に、その旨を明確に伝えるメッセージが生成されます。

src/pkg/crypto/tls/generate_cert.go

このファイルは、テスト目的などで自己署名証明書を生成するためのユーティリティです。

  1. コマンドライン引数の拡張:
    • --hostフラグが、カンマ区切りのホスト名とIPアドレスを受け入れるように変更されました。
    • --start-date--duration--ca--rsa-bitsなどの新しいフラグが追加され、証明書生成の柔軟性が向上しました。
  2. IP SANsの生成: --hostで指定された値がIPアドレスとしてパースできる場合、そのIPアドレスは生成される証明書のtemplate.IPAddressesスライスに追加されます。これにより、このツールでIP SANsを含む証明書を簡単に生成できるようになりました。

src/pkg/crypto/x509/x509_test.go

このファイルは、crypto/x509パッケージのテストケースを含んでいます。

  1. TestMatchIP関数の追加: IPアドレスの検証ロジックをテストするための新しい関数TestMatchIPが追加されました。このテストは、IPアドレスがIP SANsに対して正しく照合されること、およびDNS名やCommon Nameに対してIPアドレスが照合されないことを確認します。
  2. TestCreateSelfSignedCertificateの更新: 自己署名証明書生成のテストが更新され、生成される証明書にIP SANsとEmail SANsが含まれるように変更されました。また、パースされた証明書のDNSNamesEmailAddressesIPAddressesフィールドがテンプレートと一致するかを検証するアサーションが追加されました。

src/pkg/go/build/deps_test.go

このファイルは、Goパッケージのビルド依存関係を定義しています。

  1. crypto/x509の依存関係にnetパッケージを追加: crypto/x509パッケージがIPアドレスを扱うためにnetパッケージの機能(net.IPnet.ParseIPなど)を使用するようになったため、その依存関係が明示的に追加されました。

src/pkg/net/http/httptest/server.go

このファイルは、HTTPテストサーバーのヘルパー関数を含んでいます。

  1. localhostCertの更新: テスト用のlocalhostCertが、IP SANs(127.0.0.1::1)を含むように更新されました。これは、generate_cert.goツールを使用して新しい証明書が生成されたことを示しています。これにより、HTTPテスト環境でIP SANsの検証をテストできるようになります。

これらの変更により、Goのcrypto/x509パッケージはIPアドレスをX.509証明書のSANとして完全にサポートし、RFC 6125に準拠した堅牢な検証メカニズムを提供します。

コアとなるコードの変更箇所

src/pkg/crypto/x509/x509.go - Certificate構造体へのIPAddressesフィールド追加

type Certificate struct {
	// ...
	// Subject Alternate Name values
	DNSNames       []string
	EmailAddresses []string
	IPAddresses    []net.IP // 新しく追加されたフィールド
	// ...
}

src/pkg/crypto/x509/x509.go - parseCertificate関数でのIP SANsのパース

// parseCertificate関数内、GeneralNameのswitch文の一部
					case 7: // iPAddress
						switch len(v.Bytes) {
						case net.IPv4len, net.IPv6len:
							out.IPAddresses = append(out.IPAddresses, v.Bytes)
						default:
							return nil, errors.New("x509: certificate contained IP address of length " + strconv.Itoa(len(v.Bytes)))
						}

src/pkg/crypto/x509/x509.go - buildExtensions関数でのIP SANsのエンコード

// buildExtensions関数内、SAN拡張の構築部分
	if len(template.DNSNames) > 0 || len(template.EmailAddresses) > 0 || len(template.IPAddresses) > 0 {
		ret[n].Id = oidExtensionSubjectAltName
		var rawValues []asn1.RawValue
		for _, name := range template.DNSNames {
			rawValues = append(rawValues, asn1.RawValue{Tag: 2, Class: 2, Bytes: []byte(name)})
		}
		for _, email := range template.EmailAddresses {
			rawValues = append(rawValues, asn1.RawValue{Tag: 1, Class: 2, Bytes: []byte(email)})
		}
		for _, rawIP := range template.IPAddresses {
			// If possible, we always want to encode IPv4 addresses in 4 bytes.
			ip := rawIP.To4()
			if ip == nil {
				ip = rawIP
			}
			rawValues = append(rawValues, asn1.RawValue{Tag: 7, Class: 2, Bytes: ip})
		}
		ret[n].Value, err = asn1.Marshal(rawValues)
		if err != nil {
			return
		}
		n++
	}

src/pkg/crypto/x509/verify.go - VerifyHostname関数でのIPアドレス検証ロジック

// VerifyHostname関数内
func (c *Certificate) VerifyHostname(h string) error {
	// IP addresses may be written in [ ].
	candidateIP := h
	if len(h) >= 3 && h[0] == '[' && h[len(h)-1] == ']' {
		candidateIP = h[1 : len(h)-1]
	}
	if ip := net.ParseIP(candidateIP); ip != nil {
		// We only match IP addresses against IP SANs.
		// https://tools.ietf.org/html/rfc6125#appendix-B.2
		for _, candidate := range c.IPAddresses {
			if ip.Equal(candidate) {
				return nil
			}
			// IPv4-mapped IPv6 addresses are not considered equal to IPv4 addresses.
			// RFC 6125, Section 6.4.2, bullet 3.
		}
		return HostnameError{c, candidateIP}
	}

	lowered := toLowerCaseASCII(h)

	if len(c.DNSNames) > 0 {
		// ... 既存のDNSName検証ロジック ...
	}
	// ... 既存のCommonName検証ロジック ...
}

コアとなるコードの解説

Certificate構造体へのIPAddressesフィールド追加

これは、X.509証明書からパースされたIPアドレス形式のSANを保持するための基本的な変更です。net.IP型はGoの標準ライブラリでIPアドレスを表すために使用され、IPv4とIPv6の両方に対応できます。このフィールドの追加により、証明書内のIP SANsにプログラムから直接アクセスできるようになります。

parseCertificate関数でのIP SANsのパース

X.509証明書はASN.1 (Abstract Syntax Notation One) を使用してエンコードされます。SAN拡張内の各エントリはGeneralName構造体として表現され、その種類(DNS名、IPアドレスなど)はASN.1のタグによって識別されます。IPアドレスの場合、ASN.1のGeneralNameではtag 7が割り当てられています。

このコードは、証明書をパースする際に、SAN拡張からtag 7を持つRawValue(生のASN.1値)を検出します。検出されたRawValueのバイト列がIPアドレスの有効な長さ(IPv4の場合は4バイト、IPv6の場合は16バイト)であるかを確認し、net.IP型に変換してCertificate.IPAddressesスライスに追加します。これにより、証明書内のIP SANsがGoのデータ構造に正しくデコードされるようになります。

buildExtensions関数でのIP SANsのエンコード

このコードは、GoのCertificate構造体からX.509証明書を構築する際に、IPAddressesフィールドに格納されているIPアドレスをSAN拡張としてエンコードする役割を担います。

template.IPAddressesスライスにIPアドレスが存在する場合、それぞれのIPアドレスはASN.1のRawValueとして準備されます。ここでもtag 7がIPアドレスを示すために使用されます。特に、IPv4アドレスは、可能であれば4バイト形式でエンコードされるようにrawIP.To4()が呼び出されます。これは、IPv4アドレスがIPv6アドレスとしてマッピングされた形式(例: ::ffff:192.0.2.1)でエンコードされるのを防ぎ、よりコンパクトで標準的な表現を保証するためです。これらのRawValueはASN.1のシーケンスとしてマーシャリングされ、SAN拡張として証明書に追加されます。

VerifyHostname関数でのIPアドレス検証ロジック

この変更は、RFC 6125の主要な要件を実装するものです。VerifyHostname関数が呼び出された際に、検証対象のホスト名hがIPアドレス形式であるかをnet.ParseIPを使って最初にチェックします。

  • もしhがIPアドレスとしてパースできる場合([::1]のようなブラケット形式も考慮)、検証ロジックは証明書のIPAddressesフィールド(IP SANs)のみを対象とします。
  • 証明書のIPAddressesスライスをイテレートし、いずれかのIP SANが検証対象のIPアドレスと一致するかを確認します。ip.Equal(candidate)は、IPアドレスの比較を正確に行うためのメソッドです。
  • 一致するIP SANが見つかれば、検証は成功しnilが返されます。
  • 一致するIP SANが見つからない場合、または証明書にIP SANsが全く含まれていない場合は、HostnameErrorが返され、IPアドレスがIP SANsに対して検証されなかったことを示すエラーメッセージが生成されます。

この変更により、IPアドレスはDNS名やCommon Nameとは独立して、IP SANsに対してのみ検証されるようになり、RFC 6125に準拠したより安全で正確な証明書検証が実現されます。

関連リンク

参考にした情報源リンク