Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Fulcio username identity in SAN #2291

Merged
merged 3 commits into from Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
112 changes: 111 additions & 1 deletion pkg/cosign/certextensions.go
Expand Up @@ -15,7 +15,13 @@

package cosign

import "crypto/x509"
import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
"fmt"
)

type CertExtensions struct {
Cert *x509.Certificate
Expand All @@ -30,6 +36,8 @@ var (
CertExtensionGithubWorkflowRepository = "1.3.6.1.4.1.57264.1.5"
CertExtensionGithubWorkflowRef = "1.3.6.1.4.1.57264.1.6"

OIDOtherName = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 7}

CertExtensionMap = map[string]string{
CertExtensionOIDCIssuer: "oidcIssuer",
CertExtensionGithubWorkflowTrigger: "githubWorkflowTrigger",
Expand Down Expand Up @@ -82,3 +90,105 @@ func (ce *CertExtensions) GetCertExtensionGithubWorkflowRepository() string {
func (ce *CertExtensions) GetCertExtensionGithubWorkflowRef() string {
return ce.certExtensions()["githubWorkflowRef"]
}

// TODO: Move (un)marshalling to sigstore/sigstore
// OtherName describes a name related to a certificate which is not in one
// of the standard name formats. RFC 5280, 4.2.1.6:
//
// OtherName ::= SEQUENCE {
// type-id OBJECT IDENTIFIER,
// value [0] EXPLICIT ANY DEFINED BY type-id }
//
// OtherName for Fulcio-issued certificates only supports UTF-8 strings as values.
type OtherName struct {
ID asn1.ObjectIdentifier
Value string `asn1:"utf8,explicit,tag:0"`
}

// MarshalSANS creates a Subject Alternative Name extension
// with an OtherName sequence. RFC 5280, 4.2.1.6:
//
// SubjectAltName ::= GeneralNames
// GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
// GeneralName ::= CHOICE {
//
// otherName [0] OtherName,
// ... }
func MarshalSANS(name string, critical bool) (*pkix.Extension, error) {
haydentherapper marked this conversation as resolved.
Show resolved Hide resolved
var rawValues []asn1.RawValue
o := OtherName{
ID: OIDOtherName,
Value: name,
}
bytes, err := asn1.MarshalWithParams(o, "tag:0")
if err != nil {
return nil, err
}
rawValues = append(rawValues, asn1.RawValue{FullBytes: bytes})
haydentherapper marked this conversation as resolved.
Show resolved Hide resolved

sans, err := asn1.Marshal(rawValues)
if err != nil {
return nil, err
}
return &pkix.Extension{
Id: asn1.ObjectIdentifier{2, 5, 29, 17},
haydentherapper marked this conversation as resolved.
Show resolved Hide resolved
Critical: critical,
Value: sans,
}, nil
}

// UnmarshalSANs extracts a UTF-8 string from the OtherName
// field in the Subject Alternative Name extension.
func UnmarshalSANS(exts []pkix.Extension) (string, error) {
haydentherapper marked this conversation as resolved.
Show resolved Hide resolved
var otherNames []string

for _, e := range exts {
if !e.Id.Equal(asn1.ObjectIdentifier{2, 5, 29, 17}) {
continue
}

var seq asn1.RawValue
rest, err := asn1.Unmarshal(e.Value, &seq)
if err != nil {
return "", err
} else if len(rest) != 0 {
return "", fmt.Errorf("trailing data after X.509 extension")
}
if !seq.IsCompound || seq.Tag != 16 || seq.Class != 0 {
haydentherapper marked this conversation as resolved.
Show resolved Hide resolved
return "", asn1.StructuralError{Msg: "bad SAN sequence"}
}

rest = seq.Bytes
for len(rest) > 0 {
var v asn1.RawValue
rest, err = asn1.Unmarshal(rest, &v)
if err != nil {
return "", err
}

// skip all GeneralName fields except OtherName
if v.Tag != 0 {
haydentherapper marked this conversation as resolved.
Show resolved Hide resolved
continue
}

var other OtherName
_, err := asn1.UnmarshalWithParams(v.FullBytes, &other, "tag:0")
if err != nil {
return "", fmt.Errorf("could not parse requested OtherName SAN: %w", err)
}
if !other.ID.Equal(OIDOtherName) {
return "", fmt.Errorf("unexpected OID for OtherName, expected %v, got %v", OIDOtherName, other.ID)
}
otherNames = append(otherNames, other.Value)
}
}

if len(otherNames) == 0 {
return "", errors.New("no OtherName found")
}
if len(otherNames) != 1 {
return "", errors.New("expected only one OtherName")
}

return otherNames[0], nil
}
165 changes: 165 additions & 0 deletions pkg/cosign/certextensions_test.go
Expand Up @@ -18,6 +18,8 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/hex"
"strings"
"testing"
)

Expand Down Expand Up @@ -64,3 +66,166 @@ func TestCertExtensions(t *testing.T) {
t.Fatal("CertExtension does not extract field 'githubWorkflowRef' correctly")
}
}

func TestMarshalAndUnmarshalSANS(t *testing.T) {
otherName := "foo!example.com"
critical := true

ext, err := MarshalSANS(otherName, critical)
if err != nil {
t.Fatalf("unexpected error for MarshalSANs: %v", err)
}
if ext.Critical != critical {
t.Fatalf("expected extension to be critical")
}
if !ext.Id.Equal(asn1.ObjectIdentifier{2, 5, 29, 17}) {
t.Fatalf("expected extension's OID to be SANs OID")
}
// https://lapo.it/asn1js/#MCGgHwYKKwYBBAGDvzABB6ARDA9mb28hZXhhbXBsZS5jb20
// 30 - Constructed sequence
// 21 - length of sequence
// A0 - Context-specific (class 2) (bits 8,7) with Constructed bit (bit 6) and 0 tag
// 1F - length of context-specific field (OID)
// 06 - OID tag
// 0A - length of OID
// 2B 06 01 04 01 83 BF 30 01 07 - OID
// A0 - Context-specific (class 2) with Constructed bit and 0 tag
// (needed for EXPLICIT encoding, which wraps field in outer encoding)
// 11 - length of context-specific field (string)
// 0C - UTF8String tag
// 0F - length of string
// 66 6F 6F 21 65 78 61 6D 70 6C 65 2E 63 6F 6D - string
if hex.EncodeToString(ext.Value) != "3021a01f060a2b0601040183bf300107a0110c0f666f6f216578616d706c652e636f6d" {
t.Fatalf("unexpected ASN.1 encoding")
}

on, err := UnmarshalSANS([]pkix.Extension{*ext})
if err != nil {
t.Fatalf("unexpected error for UnmarshalSANs: %v", err)
}
if on != otherName {
t.Fatalf("unexpected OtherName, expected %s, got %s", otherName, on)
}
}

func TestUnmarshalSANsFailures(t *testing.T) {
var err error

// failure: no SANs extension
ext := &pkix.Extension{
Id: asn1.ObjectIdentifier{},
Critical: true,
Value: []byte{},
}
_, err = UnmarshalSANS([]pkix.Extension{*ext})
if err == nil || !strings.Contains(err.Error(), "no OtherName found") {
t.Fatalf("expected error finding no OtherName, got %v", err)
}

// failure: bad sequence
ext = &pkix.Extension{
Id: asn1.ObjectIdentifier{2, 5, 29, 17},
Critical: true,
Value: []byte{},
}
_, err = UnmarshalSANS([]pkix.Extension{*ext})
if err == nil || !strings.Contains(err.Error(), "sequence truncated") {
t.Fatalf("expected error with invalid ASN.1, got %v", err)
}

// failure: extra data after valid sequence
b, _ := hex.DecodeString("3021a01f060a2b0601040183bf300107a0110c0f666f6f216578616d706c652e636f6d" + "30")
ext = &pkix.Extension{
Id: asn1.ObjectIdentifier{2, 5, 29, 17},
Critical: true,
Value: b,
}
_, err = UnmarshalSANS([]pkix.Extension{*ext})
if err == nil || !strings.Contains(err.Error(), "trailing data after X.509 extension") {
t.Fatalf("expected error with extra data, got %v", err)
}

// failure: non-universal class (Change last two bits: 30 = 00110000 => 10110000 -> B0)
b, _ = hex.DecodeString("B021a01f060a2b0601040183bf300107a0110c0f666f6f216578616d706c652e636f6d")
ext = &pkix.Extension{
Id: asn1.ObjectIdentifier{2, 5, 29, 17},
Critical: true,
Value: b,
}
_, err = UnmarshalSANS([]pkix.Extension{*ext})
if err == nil || !strings.Contains(err.Error(), "bad SAN sequence") {
t.Fatalf("expected error with non-universal class, got %v", err)
}

// failure: not compound sequence (Change 6th bit: 30 = 00110000 => 00010000 -> 10)
b, _ = hex.DecodeString("1021a01f060a2b0601040183bf300107a0110c0f666f6f216578616d706c652e636f6d")
ext = &pkix.Extension{
Id: asn1.ObjectIdentifier{2, 5, 29, 17},
Critical: true,
Value: b,
}
_, err = UnmarshalSANS([]pkix.Extension{*ext})
if err == nil || !strings.Contains(err.Error(), "bad SAN sequence") {
t.Fatalf("expected error with non-compound sequence, got %v", err)
}

// failure: non-sequence tag (Change lower 5 bits: 30 = 00110000 => 00000010 -> 02)
b, _ = hex.DecodeString("0221a01f060a2b0601040183bf300107a0110c0f666f6f216578616d706c652e636f6d")
ext = &pkix.Extension{
Id: asn1.ObjectIdentifier{2, 5, 29, 17},
Critical: true,
Value: b,
}
_, err = UnmarshalSANS([]pkix.Extension{*ext})
if err == nil || !strings.Contains(err.Error(), "bad SAN sequence") {
t.Fatalf("expected error with non-sequence tag, got %v", err)
}

// failure: no GeneralName with tag=0 (Change lower 5 bits of first sequence field: 3021a01f -> 3021a11f)
b, _ = hex.DecodeString("3021a11f060a2b0601040183bf300108a0110c0f666f6f216578616d706c652e636f6d")
ext = &pkix.Extension{
Id: asn1.ObjectIdentifier{2, 5, 29, 17},
Critical: true,
Value: b,
}
_, err = UnmarshalSANS([]pkix.Extension{*ext})
if err == nil || !strings.Contains(err.Error(), "no OtherName found") {
t.Fatalf("expected error with no GeneralName, got %v", err)
}

// failure: invalid OtherName (Change tag of UTF8String field to 1: a0110c0f -> a1110c0f)
b, _ = hex.DecodeString("3021a01f060a2b0601040183bf300108a1110c0f666f6f216578616d706c652e636f6d")
ext = &pkix.Extension{
Id: asn1.ObjectIdentifier{2, 5, 29, 17},
Critical: true,
Value: b,
}
_, err = UnmarshalSANS([]pkix.Extension{*ext})
if err == nil || !strings.Contains(err.Error(), "could not parse requested OtherName SAN") {
t.Fatalf("expected error with invalid OtherName, got %v", err)
}

// failure: OtherName has wrong OID (2b0601040183bf300107 -> 2b0601040183bf300108)
b, _ = hex.DecodeString("3021a01f060a2b0601040183bf300108a0110c0f666f6f216578616d706c652e636f6d")
ext = &pkix.Extension{
Id: asn1.ObjectIdentifier{2, 5, 29, 17},
Critical: true,
Value: b,
}
_, err = UnmarshalSANS([]pkix.Extension{*ext})
if err == nil || !strings.Contains(err.Error(), "unexpected OID for OtherName") {
t.Fatalf("expected error with wrong OID, got %v", err)
}

// failure: multiple OtherName fields (Increase sequence size from 0x21 -> 0x42, duplicate OtherName)
b, _ = hex.DecodeString("3042a01f060a2b0601040183bf300107a0110c0f666f6f216578616d706c652e636f6da01f060a2b0601040183bf300107a0110c0f666f6f216578616d706c652e636f6d")
ext = &pkix.Extension{
Id: asn1.ObjectIdentifier{2, 5, 29, 17},
Critical: true,
Value: b,
}
_, err = UnmarshalSANS([]pkix.Extension{*ext})
if err == nil || !strings.Contains(err.Error(), "expected only one OtherName") {
t.Fatalf("expected error with multiple OtherName fields, got %v", err)
}
}
19 changes: 19 additions & 0 deletions pkg/cosign/verify.go
Expand Up @@ -21,6 +21,7 @@ import (
"crypto/ecdsa"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/hex"
"encoding/json"
Expand Down Expand Up @@ -169,6 +170,20 @@ func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Ver
return nil, fmt.Errorf("invalid certificate found on signature: %w", err)
}

// Handle certificates where the Subject Alternative Name is not set to a supported
// GeneralName (RFC 5280 4.2.1.6). Go only supports DNS, IP addresses, email addresses,
// or URIs as SANs. Fulcio can issue a certificate with an OtherName GeneralName, so
// remove the unhandled critical SAN extension before verifying.
if len(cert.UnhandledCriticalExtensions) > 0 {
var unhandledExts []asn1.ObjectIdentifier
for _, oid := range cert.UnhandledCriticalExtensions {
if !oid.Equal(asn1.ObjectIdentifier{2, 5, 29, 17}) {
unhandledExts = append(unhandledExts, oid)
}
}
cert.UnhandledCriticalExtensions = unhandledExts
}

// Now verify the cert, then the signature.
chains, err := TrustedCert(cert, co.RootCerts, co.IntermediateCerts)
if err != nil {
Expand Down Expand Up @@ -332,6 +347,10 @@ func getSubjectAlternateNames(cert *x509.Certificate) []string {
for _, uri := range cert.URIs {
sans = append(sans, uri.String())
}
otherName, _ := UnmarshalSANS(cert.Extensions)
haydentherapper marked this conversation as resolved.
Show resolved Hide resolved
if len(otherName) > 0 {
sans = append(sans, otherName)
}
return sans
}

Expand Down