Switch to loglist3 package for parsing CT log list (#7930)

The schema tool used to parse log_list_schema.json doesn't work well
with the updated schema. This is going to be required to support
static-ct-api logs from current Chrome log lists.

Instead, use the loglist3 package inside the certificate-transparency-go
project, which Boulder already uses for CT submission otherwise.

As well, the Log IDs and keys returned from loglist3 have already been
base64 decoded, so this re-encodes them to minimize the impact on the
rest of the codebase and keep this change small.

The test log_list.json file needed to be made a bit more realistic for
loglist3 to parse without base64 or date parsing errors.
This commit is contained in:
Matthew McPherrin 2025-01-10 13:29:40 -08:00 committed by GitHub
parent e4668b4ca7
commit 8a01611b70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 2028 additions and 677 deletions

View File

@ -2,7 +2,7 @@ package loglist
import (
_ "embed"
"encoding/json"
"encoding/base64"
"errors"
"fmt"
"math/rand/v2"
@ -10,7 +10,7 @@ import (
"strings"
"time"
"github.com/letsencrypt/boulder/ctpolicy/loglist/schema"
"github.com/google/certificate-transparency-go/loglist3"
)
// purpose is the use to which a log list will be put. This type exists to allow
@ -52,53 +52,19 @@ type Log struct {
Key string
StartInclusive time.Time
EndExclusive time.Time
State state
}
// State is an enum representing the various states a CT log can be in. Only
// pending, qualified, and usable logs can be submitted to. Only usable and
// readonly logs are trusted by Chrome.
type state int
const (
unknown state = iota
pending
qualified
usable
readonly
retired
rejected
)
func stateFromState(s *schema.LogListSchemaJsonOperatorsElemLogsElemState) state {
if s == nil {
return unknown
} else if s.Rejected != nil {
return rejected
} else if s.Retired != nil {
return retired
} else if s.Readonly != nil {
return readonly
} else if s.Pending != nil {
return pending
} else if s.Qualified != nil {
return qualified
} else if s.Usable != nil {
return usable
}
return unknown
State loglist3.LogStatus
}
// usableForPurpose returns true if the log state is acceptable for the given
// log list purpose, and false otherwise.
func usableForPurpose(s state, p purpose) bool {
func usableForPurpose(s loglist3.LogStatus, p purpose) bool {
switch p {
case Issuance:
return s == usable
return s == loglist3.UsableLogStatus
case Informational:
return s == usable || s == qualified || s == pending
return s == loglist3.UsableLogStatus || s == loglist3.QualifiedLogStatus || s == loglist3.PendingLogStatus
case Validation:
return s == usable || s == readonly
return s == loglist3.UsableLogStatus || s == loglist3.ReadOnlyLogStatus
}
return false
}
@ -118,8 +84,7 @@ func New(path string) (List, error) {
// newHelper is a helper to allow the core logic of `New()` to be unit tested
// without having to write files to disk.
func newHelper(file []byte) (List, error) {
var parsed schema.LogListSchemaJson
err := json.Unmarshal(file, &parsed)
parsed, err := loglist3.NewFromJSON(file)
if err != nil {
return nil, fmt.Errorf("failed to parse CT Log List: %w", err)
}
@ -128,34 +93,19 @@ func newHelper(file []byte) (List, error) {
for _, op := range parsed.Operators {
group := make(OperatorGroup)
for _, log := range op.Logs {
var name string
if log.Description != nil {
name = *log.Description
}
info := Log{
Name: name,
Url: log.Url,
Key: log.Key,
State: stateFromState(log.State),
Name: log.Description,
Url: log.URL,
Key: base64.StdEncoding.EncodeToString(log.Key),
State: log.State.LogStatus(),
}
if log.TemporalInterval != nil {
startInclusive, err := time.Parse(time.RFC3339, log.TemporalInterval.StartInclusive)
if err != nil {
return nil, fmt.Errorf("failed to parse log %q start timestamp: %w", log.Url, err)
}
endExclusive, err := time.Parse(time.RFC3339, log.TemporalInterval.EndExclusive)
if err != nil {
return nil, fmt.Errorf("failed to parse log %q end timestamp: %w", log.Url, err)
}
info.StartInclusive = startInclusive
info.EndExclusive = endExclusive
info.StartInclusive = log.TemporalInterval.StartInclusive
info.EndExclusive = log.TemporalInterval.EndExclusive
}
group[log.LogId] = info
group[base64.StdEncoding.EncodeToString(log.LogID)] = info
}
result[op.Name] = group
}

View File

@ -4,6 +4,8 @@ import (
"testing"
"time"
"github.com/google/certificate-transparency-go/loglist3"
"github.com/letsencrypt/boulder/test"
)
@ -56,24 +58,24 @@ func TestSubset(t *testing.T) {
func TestForPurpose(t *testing.T) {
input := List{
"Operator A": {
"ID A1": Log{Name: "Log A1", State: usable},
"ID A2": Log{Name: "Log A2", State: rejected},
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
"ID A2": Log{Name: "Log A2", State: loglist3.RejectedLogStatus},
},
"Operator B": {
"ID B1": Log{Name: "Log B1", State: usable},
"ID B2": Log{Name: "Log B2", State: retired},
"ID B1": Log{Name: "Log B1", State: loglist3.UsableLogStatus},
"ID B2": Log{Name: "Log B2", State: loglist3.RetiredLogStatus},
},
"Operator C": {
"ID C1": Log{Name: "Log C1", State: pending},
"ID C2": Log{Name: "Log C2", State: readonly},
"ID C1": Log{Name: "Log C1", State: loglist3.PendingLogStatus},
"ID C2": Log{Name: "Log C2", State: loglist3.ReadOnlyLogStatus},
},
}
expected := List{
"Operator A": {
"ID A1": Log{Name: "Log A1", State: usable},
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
},
"Operator B": {
"ID B1": Log{Name: "Log B1", State: usable},
"ID B1": Log{Name: "Log B1", State: loglist3.UsableLogStatus},
},
}
actual, err := input.forPurpose(Issuance)
@ -82,16 +84,16 @@ func TestForPurpose(t *testing.T) {
input = List{
"Operator A": {
"ID A1": Log{Name: "Log A1", State: usable},
"ID A2": Log{Name: "Log A2", State: rejected},
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
"ID A2": Log{Name: "Log A2", State: loglist3.RejectedLogStatus},
},
"Operator B": {
"ID B1": Log{Name: "Log B1", State: qualified},
"ID B2": Log{Name: "Log B2", State: retired},
"ID B1": Log{Name: "Log B1", State: loglist3.QualifiedLogStatus},
"ID B2": Log{Name: "Log B2", State: loglist3.RetiredLogStatus},
},
"Operator C": {
"ID C1": Log{Name: "Log C1", State: pending},
"ID C2": Log{Name: "Log C2", State: readonly},
"ID C1": Log{Name: "Log C1", State: loglist3.PendingLogStatus},
"ID C2": Log{Name: "Log C2", State: loglist3.ReadOnlyLogStatus},
},
}
_, err = input.forPurpose(Issuance)
@ -99,10 +101,10 @@ func TestForPurpose(t *testing.T) {
expected = List{
"Operator A": {
"ID A1": Log{Name: "Log A1", State: usable},
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
},
"Operator C": {
"ID C2": Log{Name: "Log C2", State: readonly},
"ID C2": Log{Name: "Log C2", State: loglist3.ReadOnlyLogStatus},
},
}
actual, err = input.forPurpose(Validation)
@ -111,13 +113,13 @@ func TestForPurpose(t *testing.T) {
expected = List{
"Operator A": {
"ID A1": Log{Name: "Log A1", State: usable},
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
},
"Operator B": {
"ID B1": Log{Name: "Log B1", State: qualified},
"ID B1": Log{Name: "Log B1", State: loglist3.QualifiedLogStatus},
},
"Operator C": {
"ID C1": Log{Name: "Log C1", State: pending},
"ID C1": Log{Name: "Log C1", State: loglist3.PendingLogStatus},
},
}
actual, err = input.forPurpose(Informational)
@ -128,10 +130,10 @@ func TestForPurpose(t *testing.T) {
func TestOperatorForLogID(t *testing.T) {
input := List{
"Operator A": {
"ID A1": Log{Name: "Log A1", State: usable},
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
},
"Operator B": {
"ID B1": Log{Name: "Log B1", State: qualified},
"ID B1": Log{Name: "Log B1", State: loglist3.QualifiedLogStatus},
},
}
@ -146,16 +148,16 @@ func TestOperatorForLogID(t *testing.T) {
func TestPermute(t *testing.T) {
input := List{
"Operator A": {
"ID A1": Log{Name: "Log A1", State: usable},
"ID A2": Log{Name: "Log A2", State: rejected},
"ID A1": Log{Name: "Log A1", State: loglist3.UsableLogStatus},
"ID A2": Log{Name: "Log A2", State: loglist3.RejectedLogStatus},
},
"Operator B": {
"ID B1": Log{Name: "Log B1", State: qualified},
"ID B2": Log{Name: "Log B2", State: retired},
"ID B1": Log{Name: "Log B1", State: loglist3.QualifiedLogStatus},
"ID B2": Log{Name: "Log B2", State: loglist3.RetiredLogStatus},
},
"Operator C": {
"ID C1": Log{Name: "Log C1", State: pending},
"ID C2": Log{Name: "Log C2", State: readonly},
"ID C1": Log{Name: "Log C1", State: loglist3.PendingLogStatus},
"ID C2": Log{Name: "Log C2", State: loglist3.ReadOnlyLogStatus},
},
}

View File

@ -1,280 +0,0 @@
{
"type": "object",
"id": "https://www.gstatic.com/ct/log_list/v3/log_list_schema.json",
"$schema": "http://json-schema.org/draft-07/schema",
"required": [
"operators"
],
"definitions": {
"state": {
"type": "object",
"properties": {
"timestamp": {
"description": "The time at which the log entered this state.",
"type": "string",
"format": "date-time",
"examples": [
"2018-01-01T00:00:00Z"
]
}
},
"required": [
"timestamp"
]
}
},
"properties": {
"version": {
"type": "string",
"title": "Version of this log list",
"description": "The version will change whenever a change is made to any part of this log list.",
"examples": [
"1",
"1.0.0",
"1.0.0b"
]
},
"log_list_timestamp": {
"description": "The time at which this version of the log list was published.",
"type": "string",
"format": "date-time",
"examples": [
"2018-01-01T00:00:00Z"
]
},
"operators": {
"title": "CT log operators",
"description": "People/organizations that run Certificate Transparency logs.",
"type": "array",
"items": {
"type": "object",
"required": [
"name",
"email",
"logs"
],
"properties": {
"name": {
"title": "Name of this log operator",
"type": "string"
},
"email": {
"title": "CT log operator email addresses",
"description": "The log operator can be contacted using any of these email addresses.",
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {
"type": "string",
"format": "email"
}
},
"logs": {
"description": "Details of Certificate Transparency logs run by this operator.",
"type": "array",
"items": {
"type": "object",
"required": [
"key",
"log_id",
"mmd",
"url"
],
"properties": {
"description": {
"title": "Description of the CT log",
"description": "A human-readable description that can be used to identify this log.",
"type": "string"
},
"key": {
"title": "The public key of the CT log",
"description": "The log's public key as a DER-encoded ASN.1 SubjectPublicKeyInfo structure, then encoded as base64 (https://tools.ietf.org/html/rfc5280#section-4.1.2.7).",
"type": "string"
},
"log_id": {
"title": "The SHA-256 hash of the CT log's public key, base64-encoded",
"description": "This is the LogID found in SCTs issued by this log (https://tools.ietf.org/html/rfc6962#section-3.2).",
"type": "string",
"minLength": 44,
"maxLength": 44
},
"mmd": {
"title": "The Maximum Merge Delay, in seconds",
"description": "The CT log should not take longer than this to incorporate a certificate (https://tools.ietf.org/html/rfc6962#section-3).",
"type": "number",
"minimum": 1,
"default": 86400
},
"url": {
"title": "The base URL of the CT log's HTTP API",
"description": "The API endpoints are defined in https://tools.ietf.org/html/rfc6962#section-4.",
"type": "string",
"format": "uri",
"examples": [
"https://ct.googleapis.com/pilot/"
]
},
"dns": {
"title": "The domain name of the CT log's DNS API",
"description": "The API endpoints are defined in https://github.com/google/certificate-transparency-rfcs/blob/master/dns/draft-ct-over-dns.md.",
"type": "string",
"format": "hostname",
"examples": [
"pilot.ct.googleapis.com"
]
},
"temporal_interval": {
"description": "The log will only accept certificates that expire (have a NotAfter date) between these dates.",
"type": "object",
"required": [
"start_inclusive",
"end_exclusive"
],
"properties": {
"start_inclusive": {
"description": "All certificates must expire on this date or later.",
"type": "string",
"format": "date-time",
"examples": [
"2018-01-01T00:00:00Z"
]
},
"end_exclusive": {
"description": "All certificates must expire before this date.",
"type": "string",
"format": "date-time",
"examples": [
"2019-01-01T00:00:00Z"
]
}
}
},
"log_type": {
"description": "The purpose of this log, e.g. test.",
"type": "string",
"enum": [
"prod",
"test"
]
},
"state": {
"title": "The state of the log from the log list distributor's perspective.",
"type": "object",
"properties": {
"pending": {
"$ref": "#/definitions/state"
},
"qualified": {
"$ref": "#/definitions/state"
},
"usable": {
"$ref": "#/definitions/state"
},
"readonly": {
"allOf": [
{
"$ref": "#/definitions/state"
},
{
"required": [
"final_tree_head"
],
"properties": {
"final_tree_head": {
"description": "The tree head (tree size and root hash) at which the log was made read-only.",
"type": "object",
"required": [
"tree_size",
"sha256_root_hash"
],
"properties": {
"tree_size": {
"type": "number",
"minimum": 0
},
"sha256_root_hash": {
"type": "string",
"minLength": 44,
"maxLength": 44
}
}
}
}
}
]
},
"retired": {
"$ref": "#/definitions/state"
},
"rejected": {
"$ref": "#/definitions/state"
}
},
"oneOf": [
{
"required": [
"pending"
]
},
{
"required": [
"qualified"
]
},
{
"required": [
"usable"
]
},
{
"required": [
"readonly"
]
},
{
"required": [
"retired"
]
},
{
"required": [
"rejected"
]
}
]
},
"previous_operators": {
"title": "Previous operators that ran this log in the past, if any.",
"description": "If the log has changed operators, this will contain a list of the previous operators, along with the timestamp when they stopped operating the log.",
"type": "array",
"uniqueItems": true,
"items": {
"type": "object",
"required": [
"name",
"end_time"
],
"properties": {
"name": {
"title": "Name of the log operator",
"type": "string"
},
"end_time": {
"description": "The time at which this operator stopped operating this log.",
"type": "string",
"format": "date-time",
"examples": [
"2018-01-01T00:00:00Z"
]
}
}
}
}
}
}
}
}
}
}
}
}

View File

@ -1,269 +0,0 @@
// Code generated by github.com/atombender/go-jsonschema, DO NOT EDIT.
package schema
import "fmt"
import "encoding/json"
import "reflect"
type LogListSchemaJson struct {
// The time at which this version of the log list was published.
LogListTimestamp *string `json:"log_list_timestamp,omitempty"`
// People/organizations that run Certificate Transparency logs.
Operators []LogListSchemaJsonOperatorsElem `json:"operators"`
// The version will change whenever a change is made to any part of this log list.
Version *string `json:"version,omitempty"`
}
type LogListSchemaJsonOperatorsElem struct {
// The log operator can be contacted using any of these email addresses.
Email []string `json:"email"`
// Details of Certificate Transparency logs run by this operator.
Logs []LogListSchemaJsonOperatorsElemLogsElem `json:"logs"`
// Name corresponds to the JSON schema field "name".
Name string `json:"name"`
}
type LogListSchemaJsonOperatorsElemLogsElem struct {
// A human-readable description that can be used to identify this log.
Description *string `json:"description,omitempty"`
// The API endpoints are defined in
// https://github.com/google/certificate-transparency-rfcs/blob/master/dns/draft-ct-over-dns.md.
Dns *string `json:"dns,omitempty"`
// The log's public key as a DER-encoded ASN.1 SubjectPublicKeyInfo structure,
// then encoded as base64 (https://tools.ietf.org/html/rfc5280#section-4.1.2.7).
Key string `json:"key"`
// This is the LogID found in SCTs issued by this log
// (https://tools.ietf.org/html/rfc6962#section-3.2).
LogId string `json:"log_id"`
// The purpose of this log, e.g. test.
LogType *LogListSchemaJsonOperatorsElemLogsElemLogType `json:"log_type,omitempty"`
// The CT log should not take longer than this to incorporate a certificate
// (https://tools.ietf.org/html/rfc6962#section-3).
Mmd float64 `json:"mmd"`
// If the log has changed operators, this will contain a list of the previous
// operators, along with the timestamp when they stopped operating the log.
PreviousOperators []LogListSchemaJsonOperatorsElemLogsElemPreviousOperatorsElem `json:"previous_operators,omitempty"`
// State corresponds to the JSON schema field "state".
State *LogListSchemaJsonOperatorsElemLogsElemState `json:"state,omitempty"`
// The log will only accept certificates that expire (have a NotAfter date)
// between these dates.
TemporalInterval *LogListSchemaJsonOperatorsElemLogsElemTemporalInterval `json:"temporal_interval,omitempty"`
// The API endpoints are defined in https://tools.ietf.org/html/rfc6962#section-4.
Url string `json:"url"`
}
type LogListSchemaJsonOperatorsElemLogsElemLogType string
const LogListSchemaJsonOperatorsElemLogsElemLogTypeProd LogListSchemaJsonOperatorsElemLogsElemLogType = "prod"
const LogListSchemaJsonOperatorsElemLogsElemLogTypeTest LogListSchemaJsonOperatorsElemLogsElemLogType = "test"
type LogListSchemaJsonOperatorsElemLogsElemPreviousOperatorsElem struct {
// The time at which this operator stopped operating this log.
EndTime string `json:"end_time"`
// Name corresponds to the JSON schema field "name".
Name string `json:"name"`
}
type LogListSchemaJsonOperatorsElemLogsElemState struct {
// Pending corresponds to the JSON schema field "pending".
Pending *State `json:"pending,omitempty"`
// Qualified corresponds to the JSON schema field "qualified".
Qualified *State `json:"qualified,omitempty"`
// Readonly corresponds to the JSON schema field "readonly".
Readonly interface{} `json:"readonly,omitempty"`
// Rejected corresponds to the JSON schema field "rejected".
Rejected *State `json:"rejected,omitempty"`
// Retired corresponds to the JSON schema field "retired".
Retired *State `json:"retired,omitempty"`
// Usable corresponds to the JSON schema field "usable".
Usable *State `json:"usable,omitempty"`
}
// The log will only accept certificates that expire (have a NotAfter date) between
// these dates.
type LogListSchemaJsonOperatorsElemLogsElemTemporalInterval struct {
// All certificates must expire before this date.
EndExclusive string `json:"end_exclusive"`
// All certificates must expire on this date or later.
StartInclusive string `json:"start_inclusive"`
}
type State struct {
// The time at which the log entered this state.
Timestamp string `json:"timestamp"`
}
// UnmarshalJSON implements json.Unmarshaler.
func (j *LogListSchemaJsonOperatorsElemLogsElemPreviousOperatorsElem) UnmarshalJSON(b []byte) error {
var raw map[string]interface{}
if err := json.Unmarshal(b, &raw); err != nil {
return err
}
if v, ok := raw["end_time"]; !ok || v == nil {
return fmt.Errorf("field end_time: required")
}
if v, ok := raw["name"]; !ok || v == nil {
return fmt.Errorf("field name: required")
}
type Plain LogListSchemaJsonOperatorsElemLogsElemPreviousOperatorsElem
var plain Plain
if err := json.Unmarshal(b, &plain); err != nil {
return err
}
*j = LogListSchemaJsonOperatorsElemLogsElemPreviousOperatorsElem(plain)
return nil
}
// UnmarshalJSON implements json.Unmarshaler.
func (j *LogListSchemaJsonOperatorsElemLogsElemTemporalInterval) UnmarshalJSON(b []byte) error {
var raw map[string]interface{}
if err := json.Unmarshal(b, &raw); err != nil {
return err
}
if v, ok := raw["end_exclusive"]; !ok || v == nil {
return fmt.Errorf("field end_exclusive: required")
}
if v, ok := raw["start_inclusive"]; !ok || v == nil {
return fmt.Errorf("field start_inclusive: required")
}
type Plain LogListSchemaJsonOperatorsElemLogsElemTemporalInterval
var plain Plain
if err := json.Unmarshal(b, &plain); err != nil {
return err
}
*j = LogListSchemaJsonOperatorsElemLogsElemTemporalInterval(plain)
return nil
}
// UnmarshalJSON implements json.Unmarshaler.
func (j *LogListSchemaJsonOperatorsElemLogsElemLogType) UnmarshalJSON(b []byte) error {
var v string
if err := json.Unmarshal(b, &v); err != nil {
return err
}
var ok bool
for _, expected := range enumValues_LogListSchemaJsonOperatorsElemLogsElemLogType {
if reflect.DeepEqual(v, expected) {
ok = true
break
}
}
if !ok {
return fmt.Errorf("invalid value (expected one of %#v): %#v", enumValues_LogListSchemaJsonOperatorsElemLogsElemLogType, v)
}
*j = LogListSchemaJsonOperatorsElemLogsElemLogType(v)
return nil
}
// UnmarshalJSON implements json.Unmarshaler.
func (j *LogListSchemaJsonOperatorsElemLogsElem) UnmarshalJSON(b []byte) error {
var raw map[string]interface{}
if err := json.Unmarshal(b, &raw); err != nil {
return err
}
if v, ok := raw["key"]; !ok || v == nil {
return fmt.Errorf("field key: required")
}
if v, ok := raw["log_id"]; !ok || v == nil {
return fmt.Errorf("field log_id: required")
}
if v, ok := raw["url"]; !ok || v == nil {
return fmt.Errorf("field url: required")
}
type Plain LogListSchemaJsonOperatorsElemLogsElem
var plain Plain
if err := json.Unmarshal(b, &plain); err != nil {
return err
}
if v, ok := raw["mmd"]; !ok || v == nil {
plain.Mmd = 86400
}
*j = LogListSchemaJsonOperatorsElemLogsElem(plain)
return nil
}
// UnmarshalJSON implements json.Unmarshaler.
func (j *State) UnmarshalJSON(b []byte) error {
var raw map[string]interface{}
if err := json.Unmarshal(b, &raw); err != nil {
return err
}
if v, ok := raw["timestamp"]; !ok || v == nil {
return fmt.Errorf("field timestamp: required")
}
type Plain State
var plain Plain
if err := json.Unmarshal(b, &plain); err != nil {
return err
}
*j = State(plain)
return nil
}
// UnmarshalJSON implements json.Unmarshaler.
func (j *LogListSchemaJsonOperatorsElem) UnmarshalJSON(b []byte) error {
var raw map[string]interface{}
if err := json.Unmarshal(b, &raw); err != nil {
return err
}
if v, ok := raw["email"]; !ok || v == nil {
return fmt.Errorf("field email: required")
}
if v, ok := raw["logs"]; !ok || v == nil {
return fmt.Errorf("field logs: required")
}
if v, ok := raw["name"]; !ok || v == nil {
return fmt.Errorf("field name: required")
}
type Plain LogListSchemaJsonOperatorsElem
var plain Plain
if err := json.Unmarshal(b, &plain); err != nil {
return err
}
*j = LogListSchemaJsonOperatorsElem(plain)
return nil
}
var enumValues_LogListSchemaJsonOperatorsElemLogsElemLogType = []interface{}{
"prod",
"test",
}
// UnmarshalJSON implements json.Unmarshaler.
func (j *LogListSchemaJson) UnmarshalJSON(b []byte) error {
var raw map[string]interface{}
if err := json.Unmarshal(b, &raw); err != nil {
return err
}
if v, ok := raw["operators"]; !ok || v == nil {
return fmt.Errorf("field operators: required")
}
type Plain LogListSchemaJson
var plain Plain
if err := json.Unmarshal(b, &plain); err != nil {
return err
}
*j = LogListSchemaJson(plain)
return nil
}

View File

@ -1,24 +0,0 @@
#!/usr/bin/env bash
set -e
# This script updates the log list JSON Schema and the Go structs generated
# from that schema.
# It is not intended to be run on a regular basis; we do not expect the JSON
# Schema to change. It is retained here for historical purposes, so that if/when
# the schema does change, or the ecosystem moves to a v4 version of the schema,
# regenerating these files will be quick and easy.
# This script expects github.com/atombender/go-jsonschema to be installed:
if ! command -v gojsonschema
then
echo "Install gojsonschema, then re-run this script:"
echo "go install github.com/atombender/go-jsonschema/cmd/gojsonschema@latest"
fi
this_dir=$(dirname $(readlink -f "${0}"))
curl https://www.gstatic.com/ct/log_list/v3/log_list_schema.json >| "${this_dir}"/log_list_schema.json
gojsonschema -p schema "${this_dir}"/log_list_schema.json >| "${this_dir}"/schema.go

4
go.sum
View File

@ -155,6 +155,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/letsencrypt/borp v0.0.0-20240620175310-a78493c6e2bd h1:3c+LdlAOEcW1qmG8gtkMCyAEoslmj6XCmniB+926kMM=
github.com/letsencrypt/borp v0.0.0-20240620175310-a78493c6e2bd/go.mod h1:gMSMCNKhxox/ccR923EJsIvHeVVYfCABGbirqa0EwuM=
github.com/letsencrypt/challtestsrv v1.2.1 h1:Lzv4jM+wSgVMCeO5a/F/IzSanhClstFMnX6SfrAJXjI=
@ -226,6 +228,8 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=

View File

@ -17,7 +17,7 @@
},
"state": {
"usable": {
"timestamp": "2000-00-00T00:00:00Z"
"timestamp": "2000-01-01T00:00:00Z"
}
}
},
@ -32,7 +32,7 @@
},
"state": {
"usable": {
"timestamp": "2000-00-00T00:00:00Z"
"timestamp": "2000-01-01T00:00:00Z"
}
}
},
@ -47,7 +47,7 @@
},
"state": {
"usable": {
"timestamp": "2000-00-00T00:00:00Z"
"timestamp": "2000-01-01T00:00:00Z"
}
}
},
@ -62,7 +62,7 @@
},
"state": {
"usable": {
"timestamp": "2000-00-00T00:00:00Z"
"timestamp": "2000-01-01T00:00:00Z"
}
}
}
@ -83,7 +83,7 @@
},
"state": {
"usable": {
"timestamp": "2000-00-00T00:00:00Z"
"timestamp": "2000-01-01T00:00:00Z"
}
}
},
@ -98,7 +98,7 @@
},
"state": {
"usable": {
"timestamp": "2000-00-00T00:00:00Z"
"timestamp": "2000-01-01T00:00:00Z"
}
}
}
@ -115,7 +115,7 @@
"url": "http://boulder.service.consul:4606",
"state": {
"usable": {
"timestamp": "2000-00-00T00:00:00Z"
"timestamp": "2000-01-01T00:00:00Z"
}
}
}
@ -136,7 +136,7 @@
},
"state": {
"usable": {
"timestamp": "2000-00-00T00:00:00Z"
"timestamp": "2000-01-01T00:00:00Z"
}
}
}
@ -186,8 +186,8 @@
"logs": [
{
"description": "This Log Has Every Field To Ensure We Can Parse It",
"log_id": "BaseSixtyFourEncodingOfSHA256HashOfPublicKey=",
"key": "BaseSixtyFourEncodingOfDEREncodingOfPublicKey=",
"log_id": "ZqBFtFIQLFnYQOwJfVnZRn4To/NPZJTlOf/TLBuzXxg=",
"key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMVjHUOxzh2flagPhuEYy/AhAlpD9qqACg4fGcCxOhLU35r21CQXzKDdCHMu69QDFd6EAe8iGFsybg+Yn4/njtA==",
"url": "https://example.com/ct/",
"mmd": 86400,
"state": {
@ -206,8 +206,8 @@
},
{
"description": "This Log Is Missing State To Ensure We Can Handle It",
"log_id": "SomeOtherFakeLogID=",
"key": "SomeOtherFakeKey=",
"log_id": "gw0pzEo2G0THdJlm0i80NqV+qn0i9GnbcaBvhQOFxNc=",
"key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMVjHUOxzh2flaFPhuEYy/AhAlpD9qqzHg4fGcCxOhLU39r21CQXzKDdCHMu69QDFd6EAe8iGFsybg+Yn4/njtA==",
"url": "https://example.net/ct/",
"mmd": 86400,
"temporal_interval": {

View File

@ -0,0 +1,92 @@
// Copyright 2018 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package x509ext holds extensions types and values for minimal gossip.
package x509ext
import (
"errors"
"fmt"
"github.com/google/certificate-transparency-go/asn1"
"github.com/google/certificate-transparency-go/tls"
"github.com/google/certificate-transparency-go/x509"
ct "github.com/google/certificate-transparency-go"
)
// OIDExtensionCTSTH is the OID value for an X.509 extension that holds
// a log STH value.
// TODO(drysdale): get an official OID value
var OIDExtensionCTSTH = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 5}
// OIDExtKeyUsageCTMinimalGossip is the OID value for an extended key usage
// (EKU) that indicates a leaf certificate is used for the validation of STH
// values from public CT logs.
// TODO(drysdale): get an official OID value
var OIDExtKeyUsageCTMinimalGossip = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 6}
// LogSTHInfo is the structure that gets TLS-encoded into the X.509 extension
// identified by OIDExtensionCTSTH.
type LogSTHInfo struct {
LogURL []byte `tls:"maxlen:255"`
Version tls.Enum `tls:"maxval:255"`
TreeSize uint64
Timestamp uint64
SHA256RootHash ct.SHA256Hash
TreeHeadSignature ct.DigitallySigned
}
// LogSTHInfoFromCert retrieves the STH information embedded in a certificate.
func LogSTHInfoFromCert(cert *x509.Certificate) (*LogSTHInfo, error) {
for _, ext := range cert.Extensions {
if ext.Id.Equal(OIDExtensionCTSTH) {
var sthInfo LogSTHInfo
rest, err := tls.Unmarshal(ext.Value, &sthInfo)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal STH: %v", err)
} else if len(rest) > 0 {
return nil, fmt.Errorf("trailing data (%d bytes) after STH", len(rest))
}
return &sthInfo, nil
}
}
return nil, errors.New("no STH extension found")
}
// HasSTHInfo indicates whether a certificate has embedded STH information.
func HasSTHInfo(cert *x509.Certificate) bool {
for _, ext := range cert.Extensions {
if ext.Id.Equal(OIDExtensionCTSTH) {
return true
}
}
return false
}
// STHFromCert retrieves the STH embedded in a certificate; note the returned STH
// does not have the LogID field filled in.
func STHFromCert(cert *x509.Certificate) (*ct.SignedTreeHead, error) {
sthInfo, err := LogSTHInfoFromCert(cert)
if err != nil {
return nil, err
}
return &ct.SignedTreeHead{
Version: ct.Version(sthInfo.Version),
TreeSize: sthInfo.TreeSize,
Timestamp: sthInfo.Timestamp,
SHA256RootHash: sthInfo.SHA256RootHash,
TreeHeadSignature: sthInfo.TreeHeadSignature,
}, nil
}

View File

@ -0,0 +1,125 @@
// Copyright 2022 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package loglist3
import (
"github.com/google/certificate-transparency-go/x509"
"github.com/google/certificate-transparency-go/x509util"
"k8s.io/klog/v2"
)
// LogRoots maps Log-URLs (stated at LogList) to the pools of their accepted
// root-certificates.
type LogRoots map[string]*x509util.PEMCertPool
// Compatible creates a new LogList containing only Logs matching the temporal,
// root-acceptance and Log-status conditions.
func (ll *LogList) Compatible(cert *x509.Certificate, certRoot *x509.Certificate, roots LogRoots) LogList {
active := ll.TemporallyCompatible(cert)
return active.RootCompatible(certRoot, roots)
}
// SelectByStatus creates a new LogList containing only logs with status
// provided from the original.
func (ll *LogList) SelectByStatus(lstats []LogStatus) LogList {
var active LogList
for _, op := range ll.Operators {
activeOp := *op
activeOp.Logs = []*Log{}
for _, l := range op.Logs {
for _, lstat := range lstats {
if l.State.LogStatus() == lstat {
activeOp.Logs = append(activeOp.Logs, l)
break
}
}
}
if len(activeOp.Logs) > 0 {
active.Operators = append(active.Operators, &activeOp)
}
}
return active
}
// RootCompatible creates a new LogList containing only the logs of original
// LogList that are compatible with the provided cert, according to
// the passed in collection of per-log roots. Logs that are missing from
// the collection are treated as always compatible and included, even if
// an empty cert root is passed in.
// Cert-root when provided is expected to be CA-cert.
func (ll *LogList) RootCompatible(certRoot *x509.Certificate, roots LogRoots) LogList {
var compatible LogList
// Check whether root is a CA-cert.
if certRoot != nil && !certRoot.IsCA {
klog.Warningf("Compatible method expects fully rooted chain, while last cert of the chain provided is not root")
return compatible
}
for _, op := range ll.Operators {
compatibleOp := *op
compatibleOp.Logs = []*Log{}
for _, l := range op.Logs {
// If root set is not defined, we treat Log as compatible assuming no
// knowledge of its roots.
if _, ok := roots[l.URL]; !ok {
compatibleOp.Logs = append(compatibleOp.Logs, l)
continue
}
if certRoot == nil {
continue
}
// Check root is accepted.
if roots[l.URL].Included(certRoot) {
compatibleOp.Logs = append(compatibleOp.Logs, l)
}
}
if len(compatibleOp.Logs) > 0 {
compatible.Operators = append(compatible.Operators, &compatibleOp)
}
}
return compatible
}
// TemporallyCompatible creates a new LogList containing only the logs of
// original LogList that are compatible with the provided cert, according to
// NotAfter and TemporalInterval matching.
// Returns empty LogList if nil-cert is provided.
func (ll *LogList) TemporallyCompatible(cert *x509.Certificate) LogList {
var compatible LogList
if cert == nil {
return compatible
}
for _, op := range ll.Operators {
compatibleOp := *op
compatibleOp.Logs = []*Log{}
for _, l := range op.Logs {
if l.TemporalInterval == nil {
compatibleOp.Logs = append(compatibleOp.Logs, l)
continue
}
if cert.NotAfter.Before(l.TemporalInterval.EndExclusive) && (cert.NotAfter.After(l.TemporalInterval.StartInclusive) || cert.NotAfter.Equal(l.TemporalInterval.StartInclusive)) {
compatibleOp.Logs = append(compatibleOp.Logs, l)
}
}
if len(compatibleOp.Logs) > 0 {
compatible.Operators = append(compatible.Operators, &compatibleOp)
}
}
return compatible
}

View File

@ -0,0 +1,388 @@
// Copyright 2022 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package loglist3 allows parsing and searching of the master CT Log list.
// It expects the log list to conform to the v3 schema.
package loglist3
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"regexp"
"strings"
"time"
"unicode"
"github.com/google/certificate-transparency-go/tls"
)
const (
// LogListURL has the master URL for Google Chrome's log list.
LogListURL = "https://www.gstatic.com/ct/log_list/v3/log_list.json"
// LogListSignatureURL has the URL for the signature over Google Chrome's log list.
LogListSignatureURL = "https://www.gstatic.com/ct/log_list/v3/log_list.sig"
// AllLogListURL has the URL for the list of all known logs (which isn't signed).
AllLogListURL = "https://www.gstatic.com/ct/log_list/v3/all_logs_list.json"
)
// Manually mapped from https://www.gstatic.com/ct/log_list/v3/log_list_schema.json
// LogList holds a collection of CT logs, grouped by operator.
type LogList struct {
// Version is the version of the log list.
Version string `json:"version,omitempty"`
// LogListTimestamp is the time at which the log list was published.
LogListTimestamp time.Time `json:"log_list_timestamp,omitempty"`
// Operators is a list of CT log operators and the logs they operate.
Operators []*Operator `json:"operators"`
}
// Operator holds a collection of CT logs run by the same organisation.
// It also provides information about that organisation, e.g. contact details.
type Operator struct {
// Name is the name of the CT log operator.
Name string `json:"name"`
// Email lists the email addresses that can be used to contact this log
// operator.
Email []string `json:"email"`
// Logs is a list of CT logs run by this operator.
Logs []*Log `json:"logs"`
}
// Log describes a single CT log.
type Log struct {
// Description is a human-readable string that describes the log.
Description string `json:"description,omitempty"`
// LogID is the SHA-256 hash of the log's public key.
LogID []byte `json:"log_id"`
// Key is the public key with which signatures can be verified.
Key []byte `json:"key"`
// URL is the address of the HTTPS API.
URL string `json:"url"`
// DNS is the address of the DNS API.
DNS string `json:"dns,omitempty"`
// MMD is the Maximum Merge Delay, in seconds. All submitted
// certificates must be incorporated into the log within this time.
MMD int32 `json:"mmd"`
// PreviousOperators is a list of previous operators and the timestamp
// of when they stopped running the log.
PreviousOperators []*PreviousOperator `json:"previous_operators,omitempty"`
// State is the current state of the log, from the perspective of the
// log list distributor.
State *LogStates `json:"state,omitempty"`
// TemporalInterval, if set, indicates that this log only accepts
// certificates with a NotAfter date in this time range.
TemporalInterval *TemporalInterval `json:"temporal_interval,omitempty"`
// Type indicates the purpose of this log, e.g. "test" or "prod".
Type string `json:"log_type,omitempty"`
}
// PreviousOperator holds information about a log operator and the time at which
// they stopped running a log.
type PreviousOperator struct {
// Name is the name of the CT log operator.
Name string `json:"name"`
// EndTime is the time at which the operator stopped running a log.
EndTime time.Time `json:"end_time"`
}
// TemporalInterval is a time range.
type TemporalInterval struct {
// StartInclusive is the beginning of the time range.
StartInclusive time.Time `json:"start_inclusive"`
// EndExclusive is just after the end of the time range.
EndExclusive time.Time `json:"end_exclusive"`
}
// LogStatus indicates Log status.
type LogStatus int
// LogStatus values
const (
UndefinedLogStatus LogStatus = iota
PendingLogStatus
QualifiedLogStatus
UsableLogStatus
ReadOnlyLogStatus
RetiredLogStatus
RejectedLogStatus
)
//go:generate stringer -type=LogStatus
// LogStates are the states that a CT log can be in, from the perspective of a
// user agent. Only one should be set - this is the current state.
type LogStates struct {
// Pending indicates that the log is in the "pending" state.
Pending *LogState `json:"pending,omitempty"`
// Qualified indicates that the log is in the "qualified" state.
Qualified *LogState `json:"qualified,omitempty"`
// Usable indicates that the log is in the "usable" state.
Usable *LogState `json:"usable,omitempty"`
// ReadOnly indicates that the log is in the "readonly" state.
ReadOnly *ReadOnlyLogState `json:"readonly,omitempty"`
// Retired indicates that the log is in the "retired" state.
Retired *LogState `json:"retired,omitempty"`
// Rejected indicates that the log is in the "rejected" state.
Rejected *LogState `json:"rejected,omitempty"`
}
// LogState contains details on the current state of a CT log.
type LogState struct {
// Timestamp is the time when the state began.
Timestamp time.Time `json:"timestamp"`
}
// ReadOnlyLogState contains details on the current state of a read-only CT log.
type ReadOnlyLogState struct {
LogState
// FinalTreeHead is the root hash and tree size at which the CT log was
// made read-only. This should never change while the log is read-only.
FinalTreeHead TreeHead `json:"final_tree_head"`
}
// TreeHead is the root hash and tree size of a CT log.
type TreeHead struct {
// SHA256RootHash is the root hash of the CT log's Merkle tree.
SHA256RootHash []byte `json:"sha256_root_hash"`
// TreeSize is the size of the CT log's Merkle tree.
TreeSize int64 `json:"tree_size"`
}
// LogStatus method returns Log-status enum value for descriptive struct.
func (ls *LogStates) LogStatus() LogStatus {
switch {
case ls == nil:
return UndefinedLogStatus
case ls.Pending != nil:
return PendingLogStatus
case ls.Qualified != nil:
return QualifiedLogStatus
case ls.Usable != nil:
return UsableLogStatus
case ls.ReadOnly != nil:
return ReadOnlyLogStatus
case ls.Retired != nil:
return RetiredLogStatus
case ls.Rejected != nil:
return RejectedLogStatus
default:
return UndefinedLogStatus
}
}
// String method returns printable name of the state.
func (ls *LogStates) String() string {
return ls.LogStatus().String()
}
// Active picks the set-up state. If multiple states are set (not expected) picks one of them.
func (ls *LogStates) Active() (*LogState, *ReadOnlyLogState) {
if ls == nil {
return nil, nil
}
switch {
case ls.Pending != nil:
return ls.Pending, nil
case ls.Qualified != nil:
return ls.Qualified, nil
case ls.Usable != nil:
return ls.Usable, nil
case ls.ReadOnly != nil:
return nil, ls.ReadOnly
case ls.Retired != nil:
return ls.Retired, nil
case ls.Rejected != nil:
return ls.Rejected, nil
default:
return nil, nil
}
}
// GoogleOperated returns whether Operator is considered to be Google.
func (op *Operator) GoogleOperated() bool {
for _, email := range op.Email {
if strings.Contains(email, "google-ct-logs@googlegroups") {
return true
}
}
return false
}
// NewFromJSON creates a LogList from JSON encoded data.
func NewFromJSON(llData []byte) (*LogList, error) {
var ll LogList
if err := json.Unmarshal(llData, &ll); err != nil {
return nil, fmt.Errorf("failed to parse log list: %v", err)
}
return &ll, nil
}
// NewFromSignedJSON creates a LogList from JSON encoded data, checking a
// signature along the way. The signature data should be provided as the
// raw signature data.
func NewFromSignedJSON(llData, rawSig []byte, pubKey crypto.PublicKey) (*LogList, error) {
var sigAlgo tls.SignatureAlgorithm
switch pkType := pubKey.(type) {
case *rsa.PublicKey:
sigAlgo = tls.RSA
case *ecdsa.PublicKey:
sigAlgo = tls.ECDSA
default:
return nil, fmt.Errorf("unsupported public key type %v", pkType)
}
tlsSig := tls.DigitallySigned{
Algorithm: tls.SignatureAndHashAlgorithm{
Hash: tls.SHA256,
Signature: sigAlgo,
},
Signature: rawSig,
}
if err := tls.VerifySignature(pubKey, llData, tlsSig); err != nil {
return nil, fmt.Errorf("failed to verify signature: %v", err)
}
return NewFromJSON(llData)
}
// FindLogByName returns all logs whose names contain the given string.
func (ll *LogList) FindLogByName(name string) []*Log {
name = strings.ToLower(name)
var results []*Log
for _, op := range ll.Operators {
for _, log := range op.Logs {
if strings.Contains(strings.ToLower(log.Description), name) {
results = append(results, log)
}
}
}
return results
}
// FindLogByURL finds the log with the given URL.
func (ll *LogList) FindLogByURL(url string) *Log {
for _, op := range ll.Operators {
for _, log := range op.Logs {
// Don't count trailing slashes
if strings.TrimRight(log.URL, "/") == strings.TrimRight(url, "/") {
return log
}
}
}
return nil
}
// FindLogByKeyHash finds the log with the given key hash.
func (ll *LogList) FindLogByKeyHash(keyhash [sha256.Size]byte) *Log {
for _, op := range ll.Operators {
for _, log := range op.Logs {
if bytes.Equal(log.LogID, keyhash[:]) {
return log
}
}
}
return nil
}
// FindLogByKeyHashPrefix finds all logs whose key hash starts with the prefix.
func (ll *LogList) FindLogByKeyHashPrefix(prefix string) []*Log {
var results []*Log
for _, op := range ll.Operators {
for _, log := range op.Logs {
hh := hex.EncodeToString(log.LogID[:])
if strings.HasPrefix(hh, prefix) {
results = append(results, log)
}
}
}
return results
}
// FindLogByKey finds the log with the given DER-encoded key.
func (ll *LogList) FindLogByKey(key []byte) *Log {
for _, op := range ll.Operators {
for _, log := range op.Logs {
if bytes.Equal(log.Key[:], key) {
return log
}
}
}
return nil
}
var hexDigits = regexp.MustCompile("^[0-9a-fA-F]+$")
// FuzzyFindLog tries to find logs that match the given unspecified input,
// whose format is unspecified. This generally returns a single log, but
// if text input that matches multiple log descriptions is provided, then
// multiple logs may be returned.
func (ll *LogList) FuzzyFindLog(input string) []*Log {
input = strings.Trim(input, " \t")
if logs := ll.FindLogByName(input); len(logs) > 0 {
return logs
}
if log := ll.FindLogByURL(input); log != nil {
return []*Log{log}
}
// Try assuming the input is binary data of some form. First base64:
if data, err := base64.StdEncoding.DecodeString(input); err == nil {
if len(data) == sha256.Size {
var hash [sha256.Size]byte
copy(hash[:], data)
if log := ll.FindLogByKeyHash(hash); log != nil {
return []*Log{log}
}
}
if log := ll.FindLogByKey(data); log != nil {
return []*Log{log}
}
}
// Now hex, but strip all internal whitespace first.
input = stripInternalSpace(input)
if data, err := hex.DecodeString(input); err == nil {
if len(data) == sha256.Size {
var hash [sha256.Size]byte
copy(hash[:], data)
if log := ll.FindLogByKeyHash(hash); log != nil {
return []*Log{log}
}
}
if log := ll.FindLogByKey(data); log != nil {
return []*Log{log}
}
}
// Finally, allow hex strings with an odd number of digits.
if hexDigits.MatchString(input) {
if logs := ll.FindLogByKeyHashPrefix(input); len(logs) > 0 {
return logs
}
}
return nil
}
func stripInternalSpace(input string) string {
return strings.Map(func(r rune) rune {
if !unicode.IsSpace(r) {
return r
}
return -1
}, input)
}

View File

@ -0,0 +1,29 @@
// Code generated by "stringer -type=LogStatus"; DO NOT EDIT.
package loglist3
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[UndefinedLogStatus-0]
_ = x[PendingLogStatus-1]
_ = x[QualifiedLogStatus-2]
_ = x[UsableLogStatus-3]
_ = x[ReadOnlyLogStatus-4]
_ = x[RetiredLogStatus-5]
_ = x[RejectedLogStatus-6]
}
const _LogStatus_name = "UndefinedLogStatusPendingLogStatusQualifiedLogStatusUsableLogStatusReadOnlyLogStatusRetiredLogStatusRejectedLogStatus"
var _LogStatus_index = [...]uint8{0, 18, 34, 52, 67, 84, 100, 117}
func (i LogStatus) String() string {
if i < 0 || i >= LogStatus(len(_LogStatus_index)-1) {
return "LogStatus(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _LogStatus_name[_LogStatus_index[i]:_LogStatus_index[i+1]]
}

View File

@ -0,0 +1,116 @@
// Copyright 2016 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package x509util
import (
"encoding/pem"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"github.com/google/certificate-transparency-go/x509"
)
// ReadPossiblePEMFile loads data from a file which may be in DER format
// or may be in PEM format (with the given blockname).
func ReadPossiblePEMFile(filename, blockname string) ([][]byte, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("%s: failed to read data: %v", filename, err)
}
return dePEM(data, blockname), nil
}
// ReadPossiblePEMURL attempts to determine if the given target is a local file or a
// URL, and return the file contents regardless. It also copes with either PEM or DER
// format data.
func ReadPossiblePEMURL(target, blockname string) ([][]byte, error) {
if !strings.HasPrefix(target, "http://") && !strings.HasPrefix(target, "https://") {
// Assume it's a filename
return ReadPossiblePEMFile(target, blockname)
}
rsp, err := http.Get(target)
if err != nil {
return nil, fmt.Errorf("failed to http.Get(%q): %v", target, err)
}
data, err := io.ReadAll(rsp.Body)
if err != nil {
return nil, fmt.Errorf("failed to io.ReadAll(%q): %v", target, err)
}
return dePEM(data, blockname), nil
}
func dePEM(data []byte, blockname string) [][]byte {
var results [][]byte
if strings.Contains(string(data), "BEGIN "+blockname) {
rest := data
for {
var block *pem.Block
block, rest = pem.Decode(rest)
if block == nil {
break
}
if block.Type == blockname {
results = append(results, block.Bytes)
}
}
} else {
results = append(results, data)
}
return results
}
// ReadFileOrURL returns the data from a target which may be either a filename
// or an HTTP(S) URL.
func ReadFileOrURL(target string, client *http.Client) ([]byte, error) {
u, err := url.Parse(target)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
return os.ReadFile(target)
}
rsp, err := client.Get(u.String())
if err != nil {
return nil, fmt.Errorf("failed to http.Get(%q): %v", target, err)
}
return io.ReadAll(rsp.Body)
}
// GetIssuer attempts to retrieve the issuer for a certificate, by examining
// the cert's Authority Information Access extension (if present) for the
// issuer's URL and retrieving from there.
func GetIssuer(cert *x509.Certificate, client *http.Client) (*x509.Certificate, error) {
if len(cert.IssuingCertificateURL) == 0 {
return nil, nil
}
issuerURL := cert.IssuingCertificateURL[0]
rsp, err := client.Get(issuerURL)
if err != nil || rsp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get issuer from %q: %v", issuerURL, err)
}
defer rsp.Body.Close()
body, err := io.ReadAll(rsp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read issuer from %q: %v", issuerURL, err)
}
issuers, err := x509.ParseCertificates(body)
if err != nil {
return nil, fmt.Errorf("failed to parse issuer cert: %v", err)
}
return issuers[0], nil
}

View File

@ -0,0 +1,26 @@
// Copyright 2017 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package x509util
import "github.com/google/certificate-transparency-go/x509"
// Fuzz is a go-fuzz (https://github.com/dvyukov/go-fuzz) entrypoint
// for fuzzing the parsing of X509 certificates.
func Fuzz(data []byte) int {
if _, err := x509.ParseCertificate(data); err == nil {
return 1
}
return 0
}

View File

@ -0,0 +1,120 @@
// Copyright 2016 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package x509util
import (
"crypto/sha256"
"encoding/pem"
"errors"
"fmt"
"os"
"github.com/google/certificate-transparency-go/x509"
"k8s.io/klog/v2"
)
// String for certificate blocks in BEGIN / END PEM headers
const pemCertificateBlockType string = "CERTIFICATE"
// PEMCertPool is a wrapper / extension to x509.CertPool. It allows us to access the
// raw certs, which we need to serve get-roots request and has stricter handling on loading
// certs into the pool. CertPool ignores errors if at least one cert loads correctly but
// PEMCertPool requires all certs to load.
type PEMCertPool struct {
// maps from sha-256 to certificate, used for dup detection
fingerprintToCertMap map[[sha256.Size]byte]x509.Certificate
rawCerts []*x509.Certificate
certPool *x509.CertPool
}
// NewPEMCertPool creates a new, empty, instance of PEMCertPool.
func NewPEMCertPool() *PEMCertPool {
return &PEMCertPool{fingerprintToCertMap: make(map[[sha256.Size]byte]x509.Certificate), certPool: x509.NewCertPool()}
}
// AddCert adds a certificate to a pool. Uses fingerprint to weed out duplicates.
// cert must not be nil.
func (p *PEMCertPool) AddCert(cert *x509.Certificate) {
fingerprint := sha256.Sum256(cert.Raw)
_, ok := p.fingerprintToCertMap[fingerprint]
if !ok {
p.fingerprintToCertMap[fingerprint] = *cert
p.certPool.AddCert(cert)
p.rawCerts = append(p.rawCerts, cert)
}
}
// Included indicates whether the given cert is included in the pool.
func (p *PEMCertPool) Included(cert *x509.Certificate) bool {
fingerprint := sha256.Sum256(cert.Raw)
_, ok := p.fingerprintToCertMap[fingerprint]
return ok
}
// AppendCertsFromPEM adds certs to the pool from a byte slice assumed to contain PEM encoded data.
// Skips over non certificate blocks in the data. Returns true if all certificates in the
// data were parsed and added to the pool successfully and at least one certificate was found.
func (p *PEMCertPool) AppendCertsFromPEM(pemCerts []byte) (ok bool) {
for len(pemCerts) > 0 {
var block *pem.Block
block, pemCerts = pem.Decode(pemCerts)
if block == nil {
break
}
if block.Type != pemCertificateBlockType || len(block.Headers) != 0 {
continue
}
cert, err := x509.ParseCertificate(block.Bytes)
if x509.IsFatal(err) {
klog.Warningf("error parsing PEM certificate: %v", err)
return false
}
p.AddCert(cert)
ok = true
}
return
}
// AppendCertsFromPEMFile adds certs from a file that contains concatenated PEM data.
func (p *PEMCertPool) AppendCertsFromPEMFile(pemFile string) error {
pemData, err := os.ReadFile(pemFile)
if err != nil {
return fmt.Errorf("failed to load PEM certs file: %v", err)
}
if !p.AppendCertsFromPEM(pemData) {
return errors.New("failed to parse PEM certs file")
}
return nil
}
// Subjects returns a list of the DER-encoded subjects of all of the certificates in the pool.
func (p *PEMCertPool) Subjects() (res [][]byte) {
return p.certPool.Subjects()
}
// CertPool returns the underlying CertPool.
func (p *PEMCertPool) CertPool() *x509.CertPool {
return p.certPool
}
// RawCertificates returns a list of the raw bytes of certificates that are in this pool
func (p *PEMCertPool) RawCertificates() []*x509.Certificate {
return p.rawCerts
}

View File

@ -0,0 +1,169 @@
// Copyright 2017 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package x509util
import (
"bytes"
"encoding/hex"
"fmt"
"strconv"
"github.com/google/certificate-transparency-go/x509"
"github.com/google/certificate-transparency-go/x509/pkix"
)
// RevocationReasonToString generates a string describing a revocation reason code.
func RevocationReasonToString(reason x509.RevocationReasonCode) string {
switch reason {
case x509.Unspecified:
return "Unspecified"
case x509.KeyCompromise:
return "Key Compromise"
case x509.CACompromise:
return "CA Compromise"
case x509.AffiliationChanged:
return "Affiliation Changed"
case x509.Superseded:
return "Superseded"
case x509.CessationOfOperation:
return "Cessation Of Operation"
case x509.CertificateHold:
return "Certificate Hold"
case x509.RemoveFromCRL:
return "Remove From CRL"
case x509.PrivilegeWithdrawn:
return "Privilege Withdrawn"
case x509.AACompromise:
return "AA Compromise"
default:
return strconv.Itoa(int(reason))
}
}
// CRLToString generates a string describing the given certificate revocation list.
// The output roughly resembles that from openssl crl -text.
func CRLToString(crl *x509.CertificateList) string {
var result bytes.Buffer
var showCritical = func(critical bool) {
if critical {
result.WriteString(" critical")
}
result.WriteString("\n")
}
result.WriteString("Certificate Revocation List (CRL):\n")
result.WriteString(fmt.Sprintf(" Version: %d (%#x)\n", crl.TBSCertList.Version+1, crl.TBSCertList.Version))
result.WriteString(fmt.Sprintf(" Signature Algorithm: %v\n", x509.SignatureAlgorithmFromAI(crl.TBSCertList.Signature)))
var issuer pkix.Name
issuer.FillFromRDNSequence(&crl.TBSCertList.Issuer)
result.WriteString(fmt.Sprintf(" Issuer: %v\n", NameToString(issuer)))
result.WriteString(fmt.Sprintf(" Last Update: %v\n", crl.TBSCertList.ThisUpdate))
result.WriteString(fmt.Sprintf(" Next Update: %v\n", crl.TBSCertList.NextUpdate))
if len(crl.TBSCertList.Extensions) > 0 {
result.WriteString(" CRL extensions:\n")
}
count, critical := OIDInExtensions(x509.OIDExtensionAuthorityKeyId, crl.TBSCertList.Extensions)
if count > 0 {
result.WriteString(" X509v3 Authority Key Identifier:")
showCritical(critical)
result.WriteString(fmt.Sprintf(" keyid:%v\n", hex.EncodeToString(crl.TBSCertList.AuthorityKeyID)))
}
count, critical = OIDInExtensions(x509.OIDExtensionIssuerAltName, crl.TBSCertList.Extensions)
if count > 0 {
result.WriteString(" X509v3 Issuer Alt Name:")
showCritical(critical)
result.WriteString(fmt.Sprintf(" %s\n", GeneralNamesToString(&crl.TBSCertList.IssuerAltNames)))
}
count, critical = OIDInExtensions(x509.OIDExtensionCRLNumber, crl.TBSCertList.Extensions)
if count > 0 {
result.WriteString(" X509v3 CRLNumber:")
showCritical(critical)
result.WriteString(fmt.Sprintf(" %d\n", crl.TBSCertList.CRLNumber))
}
count, critical = OIDInExtensions(x509.OIDExtensionDeltaCRLIndicator, crl.TBSCertList.Extensions)
if count > 0 {
result.WriteString(" X509v3 Delta CRL Indicator:")
showCritical(critical)
result.WriteString(fmt.Sprintf(" %d\n", crl.TBSCertList.BaseCRLNumber))
}
count, critical = OIDInExtensions(x509.OIDExtensionIssuingDistributionPoint, crl.TBSCertList.Extensions)
if count > 0 {
result.WriteString(" X509v3 Issuing Distribution Point:")
showCritical(critical)
result.WriteString(fmt.Sprintf(" %s\n", GeneralNamesToString(&crl.TBSCertList.IssuingDPFullNames)))
}
count, critical = OIDInExtensions(x509.OIDExtensionFreshestCRL, crl.TBSCertList.Extensions)
if count > 0 {
result.WriteString(" X509v3 Freshest CRL:")
showCritical(critical)
result.WriteString(" Full Name:\n")
var buf bytes.Buffer
for _, pt := range crl.TBSCertList.FreshestCRLDistributionPoint {
commaAppend(&buf, "URI:"+pt)
}
result.WriteString(fmt.Sprintf(" %v\n", buf.String()))
}
count, critical = OIDInExtensions(x509.OIDExtensionAuthorityInfoAccess, crl.TBSCertList.Extensions)
if count > 0 {
result.WriteString(" Authority Information Access:")
showCritical(critical)
var issuerBuf bytes.Buffer
for _, issuer := range crl.TBSCertList.IssuingCertificateURL {
commaAppend(&issuerBuf, "URI:"+issuer)
}
if issuerBuf.Len() > 0 {
result.WriteString(fmt.Sprintf(" CA Issuers - %v\n", issuerBuf.String()))
}
var ocspBuf bytes.Buffer
for _, ocsp := range crl.TBSCertList.OCSPServer {
commaAppend(&ocspBuf, "URI:"+ocsp)
}
if ocspBuf.Len() > 0 {
result.WriteString(fmt.Sprintf(" OCSP - %v\n", ocspBuf.String()))
}
// TODO(drysdale): Display other GeneralName types
}
result.WriteString("\n")
result.WriteString("Revoked Certificates:\n")
for _, c := range crl.TBSCertList.RevokedCertificates {
result.WriteString(fmt.Sprintf(" Serial Number: %s (0x%s)\n", c.SerialNumber.Text(10), c.SerialNumber.Text(16)))
result.WriteString(fmt.Sprintf(" Revocation Date : %v\n", c.RevocationTime))
count, critical = OIDInExtensions(x509.OIDExtensionCRLReasons, c.Extensions)
if count > 0 {
result.WriteString(" X509v3 CRL Reason Code:")
showCritical(critical)
result.WriteString(fmt.Sprintf(" %s\n", RevocationReasonToString(c.RevocationReason)))
}
count, critical = OIDInExtensions(x509.OIDExtensionInvalidityDate, c.Extensions)
if count > 0 {
result.WriteString(" Invalidity Date:")
showCritical(critical)
result.WriteString(fmt.Sprintf(" %s\n", c.InvalidityDate))
}
count, critical = OIDInExtensions(x509.OIDExtensionCertificateIssuer, c.Extensions)
if count > 0 {
result.WriteString(" Issuer:")
showCritical(critical)
result.WriteString(fmt.Sprintf(" %s\n", GeneralNamesToString(&c.Issuer)))
}
}
result.WriteString(fmt.Sprintf(" Signature Algorithm: %v\n", x509.SignatureAlgorithmFromAI(crl.SignatureAlgorithm)))
appendHexData(&result, crl.SignatureValue.Bytes, 18, " ")
result.WriteString("\n")
return result.String()
}

View File

@ -0,0 +1,900 @@
// Copyright 2016 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package x509util includes utility code for working with X.509
// certificates from the x509 package.
package x509util
import (
"bytes"
"crypto/dsa"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"net"
"strconv"
ct "github.com/google/certificate-transparency-go"
"github.com/google/certificate-transparency-go/asn1"
"github.com/google/certificate-transparency-go/gossip/minimal/x509ext"
"github.com/google/certificate-transparency-go/tls"
"github.com/google/certificate-transparency-go/x509"
"github.com/google/certificate-transparency-go/x509/pkix"
)
// OIDForStandardExtension indicates whether oid identifies a standard extension.
// Standard extensions are listed in RFC 5280 (and other RFCs).
func OIDForStandardExtension(oid asn1.ObjectIdentifier) bool {
if oid.Equal(x509.OIDExtensionSubjectKeyId) ||
oid.Equal(x509.OIDExtensionKeyUsage) ||
oid.Equal(x509.OIDExtensionExtendedKeyUsage) ||
oid.Equal(x509.OIDExtensionAuthorityKeyId) ||
oid.Equal(x509.OIDExtensionBasicConstraints) ||
oid.Equal(x509.OIDExtensionSubjectAltName) ||
oid.Equal(x509.OIDExtensionCertificatePolicies) ||
oid.Equal(x509.OIDExtensionNameConstraints) ||
oid.Equal(x509.OIDExtensionCRLDistributionPoints) ||
oid.Equal(x509.OIDExtensionIssuerAltName) ||
oid.Equal(x509.OIDExtensionSubjectDirectoryAttributes) ||
oid.Equal(x509.OIDExtensionInhibitAnyPolicy) ||
oid.Equal(x509.OIDExtensionPolicyConstraints) ||
oid.Equal(x509.OIDExtensionPolicyMappings) ||
oid.Equal(x509.OIDExtensionFreshestCRL) ||
oid.Equal(x509.OIDExtensionSubjectInfoAccess) ||
oid.Equal(x509.OIDExtensionAuthorityInfoAccess) ||
oid.Equal(x509.OIDExtensionIPPrefixList) ||
oid.Equal(x509.OIDExtensionASList) ||
oid.Equal(x509.OIDExtensionCTPoison) ||
oid.Equal(x509.OIDExtensionCTSCT) {
return true
}
return false
}
// OIDInExtensions checks whether the extension identified by oid is present in extensions
// and returns how many times it occurs together with an indication of whether any of them
// are marked critical.
func OIDInExtensions(oid asn1.ObjectIdentifier, extensions []pkix.Extension) (int, bool) {
count := 0
critical := false
for _, ext := range extensions {
if ext.Id.Equal(oid) {
count++
if ext.Critical {
critical = true
}
}
}
return count, critical
}
// String formatting for various X.509/ASN.1 types
func bitStringToString(b asn1.BitString) string { // nolint:deadcode,unused
result := hex.EncodeToString(b.Bytes)
bitsLeft := b.BitLength % 8
if bitsLeft != 0 {
result += " (" + strconv.Itoa(8-bitsLeft) + " unused bits)"
}
return result
}
func publicKeyAlgorithmToString(algo x509.PublicKeyAlgorithm) string {
// Use OpenSSL-compatible strings for the algorithms.
switch algo {
case x509.RSA:
return "rsaEncryption"
case x509.DSA:
return "dsaEncryption"
case x509.ECDSA:
return "id-ecPublicKey"
default:
return strconv.Itoa(int(algo))
}
}
// appendHexData adds a hex dump of binary data to buf, with line breaks
// after each set of count bytes, and with each new line prefixed with the
// given prefix.
func appendHexData(buf *bytes.Buffer, data []byte, count int, prefix string) {
for ii, b := range data {
if ii%count == 0 {
if ii > 0 {
buf.WriteString("\n")
}
buf.WriteString(prefix)
}
buf.WriteString(fmt.Sprintf("%02x:", b))
}
}
func curveOIDToString(oid asn1.ObjectIdentifier) (t string, bitlen int) {
switch {
case oid.Equal(x509.OIDNamedCurveP224):
return "secp224r1", 224
case oid.Equal(x509.OIDNamedCurveP256):
return "prime256v1", 256
case oid.Equal(x509.OIDNamedCurveP384):
return "secp384r1", 384
case oid.Equal(x509.OIDNamedCurveP521):
return "secp521r1", 521
case oid.Equal(x509.OIDNamedCurveP192):
return "secp192r1", 192
}
return fmt.Sprintf("%v", oid), -1
}
func publicKeyToString(_ x509.PublicKeyAlgorithm, pub interface{}) string {
var buf bytes.Buffer
switch pub := pub.(type) {
case *rsa.PublicKey:
bitlen := pub.N.BitLen()
buf.WriteString(fmt.Sprintf(" Public Key: (%d bit)\n", bitlen))
buf.WriteString(" Modulus:\n")
data := pub.N.Bytes()
appendHexData(&buf, data, 15, " ")
buf.WriteString("\n")
buf.WriteString(fmt.Sprintf(" Exponent: %d (0x%x)", pub.E, pub.E))
case *dsa.PublicKey:
buf.WriteString(" pub:\n")
appendHexData(&buf, pub.Y.Bytes(), 15, " ")
buf.WriteString("\n")
buf.WriteString(" P:\n")
appendHexData(&buf, pub.P.Bytes(), 15, " ")
buf.WriteString("\n")
buf.WriteString(" Q:\n")
appendHexData(&buf, pub.Q.Bytes(), 15, " ")
buf.WriteString("\n")
buf.WriteString(" G:\n")
appendHexData(&buf, pub.G.Bytes(), 15, " ")
case *ecdsa.PublicKey:
data := elliptic.Marshal(pub.Curve, pub.X, pub.Y)
oid, ok := x509.OIDFromNamedCurve(pub.Curve)
if !ok {
return " <unsupported elliptic curve>"
}
oidname, bitlen := curveOIDToString(oid)
buf.WriteString(fmt.Sprintf(" Public Key: (%d bit)\n", bitlen))
buf.WriteString(" pub:\n")
appendHexData(&buf, data, 15, " ")
buf.WriteString("\n")
buf.WriteString(fmt.Sprintf(" ASN1 OID: %s", oidname))
default:
buf.WriteString(fmt.Sprintf("%v", pub))
}
return buf.String()
}
func commaAppend(buf *bytes.Buffer, s string) {
if buf.Len() > 0 {
buf.WriteString(", ")
}
buf.WriteString(s)
}
func keyUsageToString(k x509.KeyUsage) string {
var buf bytes.Buffer
if k&x509.KeyUsageDigitalSignature != 0 {
commaAppend(&buf, "Digital Signature")
}
if k&x509.KeyUsageContentCommitment != 0 {
commaAppend(&buf, "Content Commitment")
}
if k&x509.KeyUsageKeyEncipherment != 0 {
commaAppend(&buf, "Key Encipherment")
}
if k&x509.KeyUsageDataEncipherment != 0 {
commaAppend(&buf, "Data Encipherment")
}
if k&x509.KeyUsageKeyAgreement != 0 {
commaAppend(&buf, "Key Agreement")
}
if k&x509.KeyUsageCertSign != 0 {
commaAppend(&buf, "Certificate Signing")
}
if k&x509.KeyUsageCRLSign != 0 {
commaAppend(&buf, "CRL Signing")
}
if k&x509.KeyUsageEncipherOnly != 0 {
commaAppend(&buf, "Encipher Only")
}
if k&x509.KeyUsageDecipherOnly != 0 {
commaAppend(&buf, "Decipher Only")
}
return buf.String()
}
func extKeyUsageToString(u x509.ExtKeyUsage) string {
switch u {
case x509.ExtKeyUsageAny:
return "Any"
case x509.ExtKeyUsageServerAuth:
return "TLS Web server authentication"
case x509.ExtKeyUsageClientAuth:
return "TLS Web client authentication"
case x509.ExtKeyUsageCodeSigning:
return "Signing of executable code"
case x509.ExtKeyUsageEmailProtection:
return "Email protection"
case x509.ExtKeyUsageIPSECEndSystem:
return "IPSEC end system"
case x509.ExtKeyUsageIPSECTunnel:
return "IPSEC tunnel"
case x509.ExtKeyUsageIPSECUser:
return "IPSEC user"
case x509.ExtKeyUsageTimeStamping:
return "Time stamping"
case x509.ExtKeyUsageOCSPSigning:
return "OCSP signing"
case x509.ExtKeyUsageMicrosoftServerGatedCrypto:
return "Microsoft server gated cryptography"
case x509.ExtKeyUsageNetscapeServerGatedCrypto:
return "Netscape server gated cryptography"
case x509.ExtKeyUsageCertificateTransparency:
return "Certificate transparency"
default:
return "Unknown"
}
}
func attributeOIDToString(oid asn1.ObjectIdentifier) string { // nolint:deadcode,unused
switch {
case oid.Equal(pkix.OIDCountry):
return "Country"
case oid.Equal(pkix.OIDOrganization):
return "Organization"
case oid.Equal(pkix.OIDOrganizationalUnit):
return "OrganizationalUnit"
case oid.Equal(pkix.OIDCommonName):
return "CommonName"
case oid.Equal(pkix.OIDSerialNumber):
return "SerialNumber"
case oid.Equal(pkix.OIDLocality):
return "Locality"
case oid.Equal(pkix.OIDProvince):
return "Province"
case oid.Equal(pkix.OIDStreetAddress):
return "StreetAddress"
case oid.Equal(pkix.OIDPostalCode):
return "PostalCode"
case oid.Equal(pkix.OIDPseudonym):
return "Pseudonym"
case oid.Equal(pkix.OIDTitle):
return "Title"
case oid.Equal(pkix.OIDDnQualifier):
return "DnQualifier"
case oid.Equal(pkix.OIDName):
return "Name"
case oid.Equal(pkix.OIDSurname):
return "Surname"
case oid.Equal(pkix.OIDGivenName):
return "GivenName"
case oid.Equal(pkix.OIDInitials):
return "Initials"
case oid.Equal(pkix.OIDGenerationQualifier):
return "GenerationQualifier"
default:
return oid.String()
}
}
// NameToString creates a string description of a pkix.Name object.
func NameToString(name pkix.Name) string {
var result bytes.Buffer
addSingle := func(prefix, item string) {
if len(item) == 0 {
return
}
commaAppend(&result, prefix)
result.WriteString(item)
}
addList := func(prefix string, items []string) {
for _, item := range items {
addSingle(prefix, item)
}
}
addList("C=", name.Country)
addList("O=", name.Organization)
addList("OU=", name.OrganizationalUnit)
addList("L=", name.Locality)
addList("ST=", name.Province)
addList("streetAddress=", name.StreetAddress)
addList("postalCode=", name.PostalCode)
addSingle("serialNumber=", name.SerialNumber)
addSingle("CN=", name.CommonName)
for _, atv := range name.Names {
value, ok := atv.Value.(string)
if !ok {
continue
}
t := atv.Type
// All of the defined attribute OIDs are of the form 2.5.4.N, and OIDAttribute is
// the 2.5.4 prefix ('id-at' in RFC 5280).
if len(t) == 4 && t[0] == pkix.OIDAttribute[0] && t[1] == pkix.OIDAttribute[1] && t[2] == pkix.OIDAttribute[2] {
// OID is 'id-at N', so check the final value to figure out which attribute.
switch t[3] {
case pkix.OIDCommonName[3], pkix.OIDSerialNumber[3], pkix.OIDCountry[3], pkix.OIDLocality[3], pkix.OIDProvince[3],
pkix.OIDStreetAddress[3], pkix.OIDOrganization[3], pkix.OIDOrganizationalUnit[3], pkix.OIDPostalCode[3]:
continue // covered by explicit fields
case pkix.OIDPseudonym[3]:
addSingle("pseudonym=", value)
continue
case pkix.OIDTitle[3]:
addSingle("title=", value)
continue
case pkix.OIDDnQualifier[3]:
addSingle("dnQualifier=", value)
continue
case pkix.OIDName[3]:
addSingle("name=", value)
continue
case pkix.OIDSurname[3]:
addSingle("surname=", value)
continue
case pkix.OIDGivenName[3]:
addSingle("givenName=", value)
continue
case pkix.OIDInitials[3]:
addSingle("initials=", value)
continue
case pkix.OIDGenerationQualifier[3]:
addSingle("generationQualifier=", value)
continue
}
}
addSingle(t.String()+"=", value)
}
return result.String()
}
// OtherNameToString creates a string description of an x509.OtherName object.
func OtherNameToString(other x509.OtherName) string {
return fmt.Sprintf("%v=%v", other.TypeID, hex.EncodeToString(other.Value.Bytes))
}
// GeneralNamesToString creates a string description of an x509.GeneralNames object.
func GeneralNamesToString(gname *x509.GeneralNames) string {
var buf bytes.Buffer
for _, name := range gname.DNSNames {
commaAppend(&buf, "DNS:"+name)
}
for _, email := range gname.EmailAddresses {
commaAppend(&buf, "email:"+email)
}
for _, name := range gname.DirectoryNames {
commaAppend(&buf, "DirName:"+NameToString(name))
}
for _, uri := range gname.URIs {
commaAppend(&buf, "URI:"+uri)
}
for _, ip := range gname.IPNets {
if ip.Mask == nil {
commaAppend(&buf, "IP Address:"+ip.IP.String())
} else {
commaAppend(&buf, "IP Address:"+ip.IP.String()+"/"+ip.Mask.String())
}
}
for _, id := range gname.RegisteredIDs {
commaAppend(&buf, "Registered ID:"+id.String())
}
for _, other := range gname.OtherNames {
commaAppend(&buf, "othername:"+OtherNameToString(other))
}
return buf.String()
}
// CertificateToString generates a string describing the given certificate.
// The output roughly resembles that from openssl x509 -text.
func CertificateToString(cert *x509.Certificate) string {
var result bytes.Buffer
result.WriteString("Certificate:\n")
result.WriteString(" Data:\n")
result.WriteString(fmt.Sprintf(" Version: %d (%#x)\n", cert.Version, cert.Version-1))
result.WriteString(fmt.Sprintf(" Serial Number: %s (0x%s)\n", cert.SerialNumber.Text(10), cert.SerialNumber.Text(16)))
result.WriteString(fmt.Sprintf(" Signature Algorithm: %v\n", cert.SignatureAlgorithm))
result.WriteString(fmt.Sprintf(" Issuer: %v\n", NameToString(cert.Issuer)))
result.WriteString(" Validity:\n")
result.WriteString(fmt.Sprintf(" Not Before: %v\n", cert.NotBefore))
result.WriteString(fmt.Sprintf(" Not After : %v\n", cert.NotAfter))
result.WriteString(fmt.Sprintf(" Subject: %v\n", NameToString(cert.Subject)))
result.WriteString(" Subject Public Key Info:\n")
result.WriteString(fmt.Sprintf(" Public Key Algorithm: %v\n", publicKeyAlgorithmToString(cert.PublicKeyAlgorithm)))
result.WriteString(fmt.Sprintf("%v\n", publicKeyToString(cert.PublicKeyAlgorithm, cert.PublicKey)))
if len(cert.Extensions) > 0 {
result.WriteString(" X509v3 extensions:\n")
}
// First display the extensions that are already cracked out
showAuthKeyID(&result, cert)
showSubjectKeyID(&result, cert)
showKeyUsage(&result, cert)
showExtendedKeyUsage(&result, cert)
showBasicConstraints(&result, cert)
showSubjectAltName(&result, cert)
showNameConstraints(&result, cert)
showCertPolicies(&result, cert)
showCRLDPs(&result, cert)
showAuthInfoAccess(&result, cert)
showSubjectInfoAccess(&result, cert)
showRPKIAddressRanges(&result, cert)
showRPKIASIdentifiers(&result, cert)
showCTPoison(&result, cert)
showCTSCT(&result, cert)
showCTLogSTHInfo(&result, cert)
showUnhandledExtensions(&result, cert)
showSignature(&result, cert)
return result.String()
}
func showCritical(result *bytes.Buffer, critical bool) {
if critical {
result.WriteString(" critical")
}
result.WriteString("\n")
}
func showAuthKeyID(result *bytes.Buffer, cert *x509.Certificate) {
count, critical := OIDInExtensions(x509.OIDExtensionAuthorityKeyId, cert.Extensions)
if count > 0 {
result.WriteString(" X509v3 Authority Key Identifier:")
showCritical(result, critical)
result.WriteString(fmt.Sprintf(" keyid:%v\n", hex.EncodeToString(cert.AuthorityKeyId)))
}
}
func showSubjectKeyID(result *bytes.Buffer, cert *x509.Certificate) {
count, critical := OIDInExtensions(x509.OIDExtensionSubjectKeyId, cert.Extensions)
if count > 0 {
result.WriteString(" X509v3 Subject Key Identifier:")
showCritical(result, critical)
result.WriteString(fmt.Sprintf(" keyid:%v\n", hex.EncodeToString(cert.SubjectKeyId)))
}
}
func showKeyUsage(result *bytes.Buffer, cert *x509.Certificate) {
count, critical := OIDInExtensions(x509.OIDExtensionKeyUsage, cert.Extensions)
if count > 0 {
result.WriteString(" X509v3 Key Usage:")
showCritical(result, critical)
result.WriteString(fmt.Sprintf(" %v\n", keyUsageToString(cert.KeyUsage)))
}
}
func showExtendedKeyUsage(result *bytes.Buffer, cert *x509.Certificate) {
count, critical := OIDInExtensions(x509.OIDExtensionExtendedKeyUsage, cert.Extensions)
if count > 0 {
result.WriteString(" X509v3 Extended Key Usage:")
showCritical(result, critical)
var usages bytes.Buffer
for _, usage := range cert.ExtKeyUsage {
commaAppend(&usages, extKeyUsageToString(usage))
}
for _, oid := range cert.UnknownExtKeyUsage {
commaAppend(&usages, oid.String())
}
result.WriteString(fmt.Sprintf(" %v\n", usages.String()))
}
}
func showBasicConstraints(result *bytes.Buffer, cert *x509.Certificate) {
count, critical := OIDInExtensions(x509.OIDExtensionBasicConstraints, cert.Extensions)
if count > 0 {
result.WriteString(" X509v3 Basic Constraints:")
showCritical(result, critical)
result.WriteString(fmt.Sprintf(" CA:%t", cert.IsCA))
if cert.MaxPathLen > 0 || cert.MaxPathLenZero {
result.WriteString(fmt.Sprintf(", pathlen:%d", cert.MaxPathLen))
}
result.WriteString("\n")
}
}
func showSubjectAltName(result *bytes.Buffer, cert *x509.Certificate) {
count, critical := OIDInExtensions(x509.OIDExtensionSubjectAltName, cert.Extensions)
if count > 0 {
result.WriteString(" X509v3 Subject Alternative Name:")
showCritical(result, critical)
var buf bytes.Buffer
for _, name := range cert.DNSNames {
commaAppend(&buf, "DNS:"+name)
}
for _, email := range cert.EmailAddresses {
commaAppend(&buf, "email:"+email)
}
for _, ip := range cert.IPAddresses {
commaAppend(&buf, "IP Address:"+ip.String())
}
result.WriteString(fmt.Sprintf(" %v\n", buf.String()))
// TODO(drysdale): include other name forms
}
}
func showNameConstraints(result *bytes.Buffer, cert *x509.Certificate) {
count, critical := OIDInExtensions(x509.OIDExtensionNameConstraints, cert.Extensions)
if count > 0 {
result.WriteString(" X509v3 Name Constraints:")
showCritical(result, critical)
if len(cert.PermittedDNSDomains) > 0 {
result.WriteString(" Permitted:\n")
var buf bytes.Buffer
for _, name := range cert.PermittedDNSDomains {
commaAppend(&buf, "DNS:"+name)
}
result.WriteString(fmt.Sprintf(" %v\n", buf.String()))
}
// TODO(drysdale): include other name forms
}
}
func showCertPolicies(result *bytes.Buffer, cert *x509.Certificate) {
count, critical := OIDInExtensions(x509.OIDExtensionCertificatePolicies, cert.Extensions)
if count > 0 {
result.WriteString(" X509v3 Certificate Policies:")
showCritical(result, critical)
for _, oid := range cert.PolicyIdentifiers {
result.WriteString(fmt.Sprintf(" Policy: %v\n", oid.String()))
// TODO(drysdale): Display any qualifiers associated with the policy
}
}
}
func showCRLDPs(result *bytes.Buffer, cert *x509.Certificate) {
count, critical := OIDInExtensions(x509.OIDExtensionCRLDistributionPoints, cert.Extensions)
if count > 0 {
result.WriteString(" X509v3 CRL Distribution Points:")
showCritical(result, critical)
result.WriteString(" Full Name:\n")
var buf bytes.Buffer
for _, pt := range cert.CRLDistributionPoints {
commaAppend(&buf, "URI:"+pt)
}
result.WriteString(fmt.Sprintf(" %v\n", buf.String()))
// TODO(drysdale): Display other GeneralNames types, plus issuer/reasons/relative-name
}
}
func showAuthInfoAccess(result *bytes.Buffer, cert *x509.Certificate) {
count, critical := OIDInExtensions(x509.OIDExtensionAuthorityInfoAccess, cert.Extensions)
if count > 0 {
result.WriteString(" Authority Information Access:")
showCritical(result, critical)
var issuerBuf bytes.Buffer
for _, issuer := range cert.IssuingCertificateURL {
commaAppend(&issuerBuf, "URI:"+issuer)
}
if issuerBuf.Len() > 0 {
result.WriteString(fmt.Sprintf(" CA Issuers - %v\n", issuerBuf.String()))
}
var ocspBuf bytes.Buffer
for _, ocsp := range cert.OCSPServer {
commaAppend(&ocspBuf, "URI:"+ocsp)
}
if ocspBuf.Len() > 0 {
result.WriteString(fmt.Sprintf(" OCSP - %v\n", ocspBuf.String()))
}
// TODO(drysdale): Display other GeneralNames types
}
}
func showSubjectInfoAccess(result *bytes.Buffer, cert *x509.Certificate) {
count, critical := OIDInExtensions(x509.OIDExtensionSubjectInfoAccess, cert.Extensions)
if count > 0 {
result.WriteString(" Subject Information Access:")
showCritical(result, critical)
var tsBuf bytes.Buffer
for _, ts := range cert.SubjectTimestamps {
commaAppend(&tsBuf, "URI:"+ts)
}
if tsBuf.Len() > 0 {
result.WriteString(fmt.Sprintf(" AD Time Stamping - %v\n", tsBuf.String()))
}
var repoBuf bytes.Buffer
for _, repo := range cert.SubjectCARepositories {
commaAppend(&repoBuf, "URI:"+repo)
}
if repoBuf.Len() > 0 {
result.WriteString(fmt.Sprintf(" CA repository - %v\n", repoBuf.String()))
}
}
}
func showAddressRange(prefix x509.IPAddressPrefix, afi uint16) string {
switch afi {
case x509.IPv4AddressFamilyIndicator, x509.IPv6AddressFamilyIndicator:
size := 4
if afi == x509.IPv6AddressFamilyIndicator {
size = 16
}
ip := make([]byte, size)
copy(ip, prefix.Bytes)
addr := net.IPNet{IP: ip, Mask: net.CIDRMask(prefix.BitLength, 8*size)}
return addr.String()
default:
return fmt.Sprintf("%x/%d", prefix.Bytes, prefix.BitLength)
}
}
func showRPKIAddressRanges(result *bytes.Buffer, cert *x509.Certificate) {
count, critical := OIDInExtensions(x509.OIDExtensionIPPrefixList, cert.Extensions)
if count > 0 {
result.WriteString(" sbgp-ipAddrBlock:")
showCritical(result, critical)
for _, blocks := range cert.RPKIAddressRanges {
afi := blocks.AFI
switch afi {
case x509.IPv4AddressFamilyIndicator:
result.WriteString(" IPv4")
case x509.IPv6AddressFamilyIndicator:
result.WriteString(" IPv6")
default:
result.WriteString(fmt.Sprintf(" %d", afi))
}
if blocks.SAFI != 0 {
result.WriteString(fmt.Sprintf(" SAFI=%d", blocks.SAFI))
}
result.WriteString(":")
if blocks.InheritFromIssuer {
result.WriteString(" inherit\n")
continue
}
result.WriteString("\n")
for _, prefix := range blocks.AddressPrefixes {
result.WriteString(fmt.Sprintf(" %s\n", showAddressRange(prefix, afi)))
}
for _, ipRange := range blocks.AddressRanges {
result.WriteString(fmt.Sprintf(" [%s, %s]\n", showAddressRange(ipRange.Min, afi), showAddressRange(ipRange.Max, afi)))
}
}
}
}
func showASIDs(result *bytes.Buffer, asids *x509.ASIdentifiers, label string) {
if asids == nil {
return
}
result.WriteString(fmt.Sprintf(" %s:\n", label))
if asids.InheritFromIssuer {
result.WriteString(" inherit\n")
return
}
for _, id := range asids.ASIDs {
result.WriteString(fmt.Sprintf(" %d\n", id))
}
for _, idRange := range asids.ASIDRanges {
result.WriteString(fmt.Sprintf(" %d-%d\n", idRange.Min, idRange.Max))
}
}
func showRPKIASIdentifiers(result *bytes.Buffer, cert *x509.Certificate) {
count, critical := OIDInExtensions(x509.OIDExtensionASList, cert.Extensions)
if count > 0 {
result.WriteString(" sbgp-autonomousSysNum:")
showCritical(result, critical)
showASIDs(result, cert.RPKIASNumbers, "Autonomous System Numbers")
showASIDs(result, cert.RPKIRoutingDomainIDs, "Routing Domain Identifiers")
}
}
func showCTPoison(result *bytes.Buffer, cert *x509.Certificate) {
count, critical := OIDInExtensions(x509.OIDExtensionCTPoison, cert.Extensions)
if count > 0 {
result.WriteString(" RFC6962 Pre-Certificate Poison:")
showCritical(result, critical)
result.WriteString(" .....\n")
}
}
func showCTSCT(result *bytes.Buffer, cert *x509.Certificate) {
count, critical := OIDInExtensions(x509.OIDExtensionCTSCT, cert.Extensions)
if count > 0 {
result.WriteString(" RFC6962 Certificate Transparency SCT:")
showCritical(result, critical)
for i, sctData := range cert.SCTList.SCTList {
result.WriteString(fmt.Sprintf(" SCT [%d]:\n", i))
var sct ct.SignedCertificateTimestamp
_, err := tls.Unmarshal(sctData.Val, &sct)
if err != nil {
appendHexData(result, sctData.Val, 16, " ")
result.WriteString("\n")
continue
}
result.WriteString(fmt.Sprintf(" Version: %d\n", sct.SCTVersion))
result.WriteString(fmt.Sprintf(" LogID: %s\n", base64.StdEncoding.EncodeToString(sct.LogID.KeyID[:])))
result.WriteString(fmt.Sprintf(" Timestamp: %d\n", sct.Timestamp))
result.WriteString(fmt.Sprintf(" Signature: %s\n", sct.Signature.Algorithm))
result.WriteString(" Signature:\n")
appendHexData(result, sct.Signature.Signature, 16, " ")
result.WriteString("\n")
}
}
}
func showCTLogSTHInfo(result *bytes.Buffer, cert *x509.Certificate) {
count, critical := OIDInExtensions(x509ext.OIDExtensionCTSTH, cert.Extensions)
if count > 0 {
result.WriteString(" Certificate Transparency STH:")
showCritical(result, critical)
sthInfo, err := x509ext.LogSTHInfoFromCert(cert)
if err != nil {
result.WriteString(" Failed to decode STH:\n")
return
}
result.WriteString(fmt.Sprintf(" LogURL: %s\n", string(sthInfo.LogURL)))
result.WriteString(fmt.Sprintf(" Version: %d\n", sthInfo.Version))
result.WriteString(fmt.Sprintf(" TreeSize: %d\n", sthInfo.TreeSize))
result.WriteString(fmt.Sprintf(" Timestamp: %d\n", sthInfo.Timestamp))
result.WriteString(" RootHash:\n")
appendHexData(result, sthInfo.SHA256RootHash[:], 16, " ")
result.WriteString("\n")
result.WriteString(fmt.Sprintf(" TreeHeadSignature: %s\n", sthInfo.TreeHeadSignature.Algorithm))
appendHexData(result, sthInfo.TreeHeadSignature.Signature, 16, " ")
result.WriteString("\n")
}
}
func showUnhandledExtensions(result *bytes.Buffer, cert *x509.Certificate) {
for _, ext := range cert.Extensions {
// Skip extensions that are already cracked out
if oidAlreadyPrinted(ext.Id) {
continue
}
result.WriteString(fmt.Sprintf(" %v:", ext.Id))
showCritical(result, ext.Critical)
appendHexData(result, ext.Value, 16, " ")
result.WriteString("\n")
}
}
func showSignature(result *bytes.Buffer, cert *x509.Certificate) {
result.WriteString(fmt.Sprintf(" Signature Algorithm: %v\n", cert.SignatureAlgorithm))
appendHexData(result, cert.Signature, 18, " ")
result.WriteString("\n")
}
// TODO(drysdale): remove this once all standard OIDs are parsed and printed.
func oidAlreadyPrinted(oid asn1.ObjectIdentifier) bool {
if oid.Equal(x509.OIDExtensionSubjectKeyId) ||
oid.Equal(x509.OIDExtensionKeyUsage) ||
oid.Equal(x509.OIDExtensionExtendedKeyUsage) ||
oid.Equal(x509.OIDExtensionAuthorityKeyId) ||
oid.Equal(x509.OIDExtensionBasicConstraints) ||
oid.Equal(x509.OIDExtensionSubjectAltName) ||
oid.Equal(x509.OIDExtensionCertificatePolicies) ||
oid.Equal(x509.OIDExtensionNameConstraints) ||
oid.Equal(x509.OIDExtensionCRLDistributionPoints) ||
oid.Equal(x509.OIDExtensionAuthorityInfoAccess) ||
oid.Equal(x509.OIDExtensionSubjectInfoAccess) ||
oid.Equal(x509.OIDExtensionIPPrefixList) ||
oid.Equal(x509.OIDExtensionASList) ||
oid.Equal(x509.OIDExtensionCTPoison) ||
oid.Equal(x509.OIDExtensionCTSCT) ||
oid.Equal(x509ext.OIDExtensionCTSTH) {
return true
}
return false
}
// CertificateFromPEM takes a certificate in PEM format and returns the
// corresponding x509.Certificate object.
func CertificateFromPEM(pemBytes []byte) (*x509.Certificate, error) {
block, rest := pem.Decode(pemBytes)
if len(rest) != 0 {
return nil, errors.New("trailing data found after PEM block")
}
if block == nil {
return nil, errors.New("PEM block is nil")
}
if block.Type != "CERTIFICATE" {
return nil, errors.New("PEM block is not a CERTIFICATE")
}
return x509.ParseCertificate(block.Bytes)
}
// CertificatesFromPEM parses one or more certificates from the given PEM data.
// The PEM certificates must be concatenated. This function can be used for
// parsing PEM-formatted certificate chains, but does not verify that the
// resulting chain is a valid certificate chain.
func CertificatesFromPEM(pemBytes []byte) ([]*x509.Certificate, error) {
var chain []*x509.Certificate
for {
var block *pem.Block
block, pemBytes = pem.Decode(pemBytes)
if block == nil {
return chain, nil
}
if block.Type != "CERTIFICATE" {
return nil, fmt.Errorf("PEM block is not a CERTIFICATE")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, errors.New("failed to parse certificate")
}
chain = append(chain, cert)
}
}
// ParseSCTsFromSCTList parses each of the SCTs contained within an SCT list.
func ParseSCTsFromSCTList(sctList *x509.SignedCertificateTimestampList) ([]*ct.SignedCertificateTimestamp, error) {
var scts []*ct.SignedCertificateTimestamp
for i, data := range sctList.SCTList {
sct, err := ExtractSCT(&data)
if err != nil {
return nil, fmt.Errorf("error extracting SCT number %d: %s", i, err)
}
scts = append(scts, sct)
}
return scts, nil
}
// ExtractSCT deserializes an SCT from a TLS-encoded SCT.
func ExtractSCT(sctData *x509.SerializedSCT) (*ct.SignedCertificateTimestamp, error) {
if sctData == nil {
return nil, errors.New("SCT is nil")
}
var sct ct.SignedCertificateTimestamp
if rest, err := tls.Unmarshal(sctData.Val, &sct); err != nil {
return nil, fmt.Errorf("error parsing SCT: %s", err)
} else if len(rest) > 0 {
return nil, fmt.Errorf("extra data (%d bytes) after serialized SCT", len(rest))
}
return &sct, nil
}
// MarshalSCTsIntoSCTList serializes SCTs into SCT list.
func MarshalSCTsIntoSCTList(scts []*ct.SignedCertificateTimestamp) (*x509.SignedCertificateTimestampList, error) {
var sctList x509.SignedCertificateTimestampList
sctList.SCTList = []x509.SerializedSCT{}
for i, sct := range scts {
if sct == nil {
return nil, fmt.Errorf("SCT number %d is nil", i)
}
encd, err := tls.Marshal(*sct)
if err != nil {
return nil, fmt.Errorf("error serializing SCT number %d: %s", i, err)
}
sctData := x509.SerializedSCT{Val: encd}
sctList.SCTList = append(sctList.SCTList, sctData)
}
return &sctList, nil
}
var pemCertificatePrefix = []byte("-----BEGIN CERTIFICATE")
// ParseSCTsFromCertificate parses any SCTs that are embedded in the
// certificate provided. The certificate bytes provided can be either DER or
// PEM, provided the PEM data starts with the PEM block marker (i.e. has no
// leading text).
func ParseSCTsFromCertificate(certBytes []byte) ([]*ct.SignedCertificateTimestamp, error) {
var cert *x509.Certificate
var err error
if bytes.HasPrefix(certBytes, pemCertificatePrefix) {
cert, err = CertificateFromPEM(certBytes)
} else {
cert, err = x509.ParseCertificate(certBytes)
}
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %s", err)
}
return ParseSCTsFromSCTList(&cert.SCTList)
}

3
vendor/modules.txt vendored
View File

@ -180,10 +180,13 @@ github.com/google/certificate-transparency-go
github.com/google/certificate-transparency-go/asn1
github.com/google/certificate-transparency-go/client
github.com/google/certificate-transparency-go/client/configpb
github.com/google/certificate-transparency-go/gossip/minimal/x509ext
github.com/google/certificate-transparency-go/jsonclient
github.com/google/certificate-transparency-go/loglist3
github.com/google/certificate-transparency-go/tls
github.com/google/certificate-transparency-go/x509
github.com/google/certificate-transparency-go/x509/pkix
github.com/google/certificate-transparency-go/x509util
# github.com/google/uuid v1.6.0
## explicit
github.com/google/uuid