Skip to content

Commit

Permalink
Start zcryptobyte
Browse files Browse the repository at this point in the history
  • Loading branch information
dadrian committed Aug 23, 2024
1 parent 4ce2360 commit 185eb2c
Show file tree
Hide file tree
Showing 10 changed files with 585 additions and 37 deletions.
1 change: 0 additions & 1 deletion v2/asn1/asn1.go

This file was deleted.

16 changes: 16 additions & 0 deletions v2/pem/pem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package pem

import (
stdlib_pem "encoding/pem"
"errors"
)

var ErrInvalidPEM = errors.New("invalid PEM")

func DecodeContents(b []byte) ([]byte, error) {
block, _ := stdlib_pem.Decode(b)
if block == nil {
return nil, ErrInvalidPEM
}
return block.Bytes, nil
}
55 changes: 55 additions & 0 deletions v2/x509/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package x509

import (
"fmt"

"golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/cryptobyte/asn1"
)

type InvalidASN1Error struct {
Reason InvalidASN1Reason
fieldName string
expected, actual interface{}

underlying error
}

func InvalidASN1(fieldName string, underlying error) error {
return InvalidASN1Error{
fieldName: fieldName,
underlying: underlying,
}
}

func MismatchedTagIn(expected asn1.Tag, in cryptobyte.String) error {
var actual interface{}
if len(in) > 0 {
actual = in[0]
}
return InvalidASN1Error{
Reason: ReasonMismatchedTag,
expected: expected,
actual: actual,
}
}

func (e InvalidASN1Error) Error() string {
if e.underlying != nil {
return fmt.Sprintf("%s: %s", e.Reason, e.underlying)
}
if e.expected != nil {
return fmt.Sprintf("%s: expected %v, got %v", e.Reason, e.expected, e.actual)
}
return fmt.Sprintf("%s", e.Reason)
}

//go:generate stringer -type=InvalidASN1Reason -trimprefix=Reason
type InvalidASN1Reason int

const (
ReasonUnknown InvalidASN1Reason = 0
ReasonInvalidInteger InvalidASN1Reason = 1
ReasonMismatchedTag InvalidASN1Reason = 2
ReasonInvalidSequence InvalidASN1Reason = 3 // TODO(dadrian): This should be a mismatched tag or something else
)
26 changes: 26 additions & 0 deletions v2/x509/invalidasn1reason_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

200 changes: 165 additions & 35 deletions v2/x509/x509.go
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
// ZCrypto Copyright 2019 Regents of the University of Michigan
//
// 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 x509 implements a lenient X509 parser
package x509

import (
"golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/cryptobyte/asn1"
"errors"

"github.com/zmap/zcrypto/v2/zcryptobyte"
"github.com/zmap/zcrypto/v2/zcryptobyte/asn1"
)

/* Certificate ::= SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signature BIT STRING }
AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL }
-- contains a value of the type
-- registered for use with the
-- algorithm object identifier value
TBSCertificate ::= SEQUENCE {
version [0] Version DEFAULT v1,
serialNumber CertificateSerialNumber,
signature AlgorithmIdentifier,
issuer Name,
validity Validity,
subject Name,
subjectPublicKeyInfo SubjectPublicKeyInfo,
issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
-- If present, version MUST be v2 or v3
subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
-- If present, version MUST be v2 or v3
extensions [3] Extensions OPTIONAL
-- If present, version MUST be v3 -- }
*/
// Certificate ::= SEQUENCE {
// tbsCertificate TBSCertificate,
// signatureAlgorithm AlgorithmIdentifier,
// signature BIT STRING }
//
// AlgorithmIdentifier ::= SEQUENCE {
// algorithm OBJECT IDENTIFIER,
// parameters ANY DEFINED BY algorithm OPTIONAL }
// -- contains a value of the type
// -- registered for use with the
// -- algorithm object identifier value

// Certificate matches the core `Certificate` SEQUENCE from RFC 5280.
type Certificate struct {
Expand All @@ -47,12 +46,39 @@ type Certificate struct {

func ParseCertificate(b []byte) (*Certificate, error) {
var c Certificate
s := cryptobyte.String(b)
s := zcryptobyte.String(b)

// A CERTFIICATE is a SEQUENCE, so pull off the header and get a pointer to
// the start of the contents of the sequence.
var contents zcryptobyte.String
var tag asn1.Tag
var n uint32
var err error

var tbsCertificate, algorithmIdentifier, signature cryptobyte.String
s.ReadASN1(&tbsCertificate, asn1.SEQUENCE)
s.ReadASN1(&algorithmIdentifier, asn1.SEQUENCE)
s.ReadASN1(&signature, asn1.BIT_STRING)
var certificate zcryptobyte.String
var totalLen uint32
n, err = s.ReadAnyASN1(&certificate, nil, &contents, &tag)
totalLen += n
if err != nil {
return &c, err
}

var tbsCertificate, algorithmIdentifier, signature zcryptobyte.String
n, err = contents.ReadAnyASN1(&tbsCertificate, nil, nil, &tag)
totalLen += n
if err != nil {
return &c, InvalidASN1("tbsCertificate", err)
}
n, err = contents.ReadAnyASN1(&algorithmIdentifier, nil, nil, &tag)
totalLen += n
if err != nil {
return &c, InvalidASN1("algorithmIdentifier", err)
}
n, err = contents.ReadAnyASN1(&signature, nil, nil, &tag)
totalLen += n
if err != nil {
return &c, InvalidASN1("signature", err)
}

c.RawTBSCertificate = tbsCertificate
c.RawSignatureAlgorithm = algorithmIdentifier
Expand All @@ -61,8 +87,23 @@ func ParseCertificate(b []byte) (*Certificate, error) {
return &c, nil
}

// TBSCertificate ::= SEQUENCE {
// version [0] Version DEFAULT v1,
// serialNumber CertificateSerialNumber,
// signature AlgorithmIdentifier,
// issuer Name,
// validity Validity,
// subject Name,
// subjectPublicKeyInfo SubjectPublicKeyInfo,
// issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
// -- If present, version MUST be v2 or v3
// subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
// -- If present, version MUST be v2 or v3
// extensions [3] Extensions OPTIONAL
// -- If present, version MUST be v3 -- }

type TBSCertificate struct {
Version int
Version int64
SerialNumber CertificateSerialNumber
Signature AlgorithmIdentifier
Issuer Name
Expand All @@ -72,6 +113,95 @@ type TBSCertificate struct {
IssuerUniqueID UniqueIdentifier
SubjectUniqueID UniqueIdentifier
Extensions Extensions

RawVersion []byte
RawSerialNumber []byte
RawSignature []byte
RawIssuer []byte
RawValidity []byte
RawSubject []byte
RawSubjectPublicKeyInfo []byte
RawIssuerUniqueID []byte
RawSubjectUniqueID []byte
RawExtensions []byte
}

func ParseTBSCertificate(b []byte) (*TBSCertificate, error) {
var tbs TBSCertificate
var err error

it := b

tbs.Version, tbs.RawVersion, err = ParseVersion(it)
if err != nil {
return nil, err
}
it = it[len(tbs.RawVersion):]

tbs.SerialNumber, tbs.RawSerialNumber, err = ParseSerialNumber(it)
if err != nil {
return nil, err
}

return &tbs, nil
}

func checkASN1Integer(b []byte) bool {
// An ASN.1 INTEGER should never be empty. It should also be "minimally
// encoded", however we're not going to enforce that here.
return len(b) > 0
}

func asn1Signed(out *int64, n []byte) bool {
length := len(n)
if length > 8 {
return false
}
for i := 0; i < length; i++ {
*out <<= 8
*out |= int64(n[i])
}
// Shift up and down in order to sign extend the result.
*out <<= 64 - uint8(length)*8
*out >>= 64 - uint8(length)*8
return true
}

func readASN1IntegerWithTag(out *zcryptobyte.String, in zcryptobyte.String, tag asn1.Tag) (v int64, err error) {
// TODO(dadrian)[2024-08-04]: The validation methods should propagate the
// real ASN.1 error up, instead of inferring it on the next line.
_, err = in.ReadTaggedASN1(nil, out, tag)
if err != nil {
return 0, err
}
ok := checkASN1Integer(*out) && asn1Signed(&v, *out)
if !ok {
return 0, asn1.ErrInvalidInteger
}
return
}

func readASN1BigIntegerAsBytes(out *zcryptobyte.String, in zcryptobyte.String) error {
return errors.New("unimplemented")
}

// ParseVersion returns an int64 representing the Version field in the
// tbsCertificate sequence.
func ParseVersion(b []byte) (v int64, raw []byte, err error) {
s := zcryptobyte.String(b)
var rawVersion zcryptobyte.String
v, err = readASN1IntegerWithTag(&rawVersion, s, asn1.Tag(0))
return v, rawVersion, err
}

func ParseSerialNumber(b []byte) (serial []byte, raw []byte, err error) {
var out zcryptobyte.String
err = readASN1BigIntegerAsBytes(&out, b)
if err != nil {
return nil, out, err
}
serial = out[1:]
return serial, out, err
}

type AlgorithmIdentifier struct {
Expand Down
31 changes: 30 additions & 1 deletion v2/x509/x509_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import (
"path"
"testing"

"github.com/zmap/zcrypto/v2/pem"
"golang.org/x/crypto/cryptobyte"
"gopkg.in/yaml.v3"
"gotest.tools/assert"
"gotest.tools/assert/cmp"
)

type Manifest struct {
Expand All @@ -29,13 +32,39 @@ func TestParseReal(t *testing.T) {

for i, testcase := range manifest.Certificates {
t.Run(fmt.Sprintf("%2d-%s", i, testcase.Name), func(t *testing.T) {
b, err := os.ReadFile(path.Join("testdata", testcase.Name))
pemBytes, err := os.ReadFile(path.Join("testdata", testcase.Name))
assert.NilError(t, err)
b, err := pem.DecodeContents(pemBytes)
assert.NilError(t, err)
c, err := ParseCertificate(b)
assert.NilError(t, err)
assert.NilError(t, err)

assert.Check(t, len(c.RawTBSCertificate) > 0)
assert.Check(t, len(c.RawSignatureAlgorithm) > 0)
assert.Check(t, len(c.RawSignature) > 0)

// The certificate should have a 4 byte "prefix" describing the core
// SEQUENCE, so the sum of the remaining lengths should be 4 less
// than the total length.
totalRawLen := len(c.RawTBSCertificate) + len(c.RawSignatureAlgorithm) + len(c.RawSignature)
assert.Check(t, cmp.Equal(len(b)-4, totalRawLen))
})
}
}

func TestParseVersion(t *testing.T) {
standardVersions := []int64{0, 1, 2}
for i, v := range standardVersions {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
var buf [3]byte
b := cryptobyte.NewBuilder(buf[:])
b.AddASN1Int64WithTag(v, 0)
enc := b.BytesOrPanic()
v, raw, err := ParseVersion(enc)
assert.NilError(t, err, "ParseVersion(%X) returned an error %s", enc, err)
assert.Check(t, cmp.DeepEqual(raw, enc))
assert.Check(t, cmp.Equal(v, int64(enc[1])))
})
}
}
Loading

0 comments on commit 185eb2c

Please sign in to comment.