Skip to content

Commit

Permalink
cert: Add AS certificate
Browse files Browse the repository at this point in the history
This PR adds the AS certificate with parsing and validation.

contributes to scionproto#2853
  • Loading branch information
oncilla committed Jul 30, 2019
1 parent 064b0f1 commit 3dbd808
Show file tree
Hide file tree
Showing 6 changed files with 628 additions and 2 deletions.
7 changes: 6 additions & 1 deletion go/lib/scrypto/cert/v2/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "go_default_library",
srcs = ["base.go"],
srcs = [
"as.go",
"base.go",
],
importpath = "github.com/scionproto/scion/go/lib/scrypto/cert/v2",
visibility = ["//visibility:public"],
deps = [
Expand All @@ -15,6 +18,8 @@ go_library(
go_test(
name = "go_default_test",
srcs = [
"as_json_test.go",
"as_test.go",
"base_json_test.go",
"base_test.go",
],
Expand Down
151 changes: 151 additions & 0 deletions go/lib/scrypto/cert/v2/as.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright 2019 Anapaya Systems
//
// 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 cert

import (
"bytes"
"encoding/json"
"errors"

"github.com/scionproto/scion/go/lib/addr"
"github.com/scionproto/scion/go/lib/common"
"github.com/scionproto/scion/go/lib/scrypto"
)

const (
// IssuerDifferentISD indicates that the issuing AS is in a different ISD.
IssuerDifferentISD = "issuing AS in different ISD"
// InvalidCertificateType indicates the certificate type is invalid.
InvalidCertificateType = "invalid certificate type"
)

var (
// ErrIssuerIANotSet indicates the issuer IA is not set.
ErrIssuerIANotSet = errors.New("issuer IA not set")
// ErrIssuerCertificateVersionNotSet indicates the issuer certificate version is not set.
ErrIssuerCertificateVersionNotSet = errors.New("issuer certificate version not set")
)

// AS is the AS certificate.
type AS struct {
Base
// Issuer holds the identifiers of the issuing issuer certificate.
Issuer IssuerCertID `json:"Issuer"`
// CertificateType ensures the correct certificate type when marshalling.
CertificateType TypeAS `json:"CertificateType"`
}

// Validate checks that the certificate is in a valid format.
func (c *AS) Validate() error {
if err := c.Base.Validate(); err != nil {
return err
}
if err := c.validateKeys(false); err != nil {
return err
}
if c.Subject.I != c.Issuer.IA.I {
return common.NewBasicError(IssuerDifferentISD, nil,
"subject", c.Subject, "issuer", c.Issuer.IA)
}
return nil
}

// UnmarshalJSON checks that all fields are set.
func (c *AS) UnmarshalJSON(b []byte) error {
var cAlias asAlias
dec := json.NewDecoder(bytes.NewReader(b))
dec.DisallowUnknownFields()
if err := dec.Decode(&cAlias); err != nil {
return err
}
if err := cAlias.checkAllSet(); err != nil {
return err
}
*c = AS{
Base: cAlias.Base,
Issuer: *cAlias.Issuer,
CertificateType: *cAlias.CertificateType,
}
return nil
}

type asAlias struct {
Base
Issuer *IssuerCertID `json:"Issuer"`
CertificateType *TypeAS `json:"CertificateType"`
}

func (c *asAlias) checkAllSet() error {
if err := c.Base.checkAllSet(); err != nil {
return err
}
switch {
case c.Issuer == nil:
return ErrIssuerNotSet
case c.CertificateType == nil:
return ErrCertificateTypeNotSet
}
return nil
}

// issuerCertIDAlias is necessary to avoid an infinite recursion when unmarshalling.
type issuerCertIDAlias IssuerCertID

// IssuerCertID identifies the issuer certificate that authenticates the AS certificate.
type IssuerCertID struct {
// IA is the subject of the issuing issuer certificate.
IA addr.IA `json:"IA"`
// CertificateVersion is the version of the issuing issuer certificate.
CertificateVersion scrypto.Version `json:"CertificateVersion"`
}

// UnmarshalJSON checks that all fields are set.
func (i *IssuerCertID) UnmarshalJSON(b []byte) error {
dec := json.NewDecoder(bytes.NewReader(b))
dec.DisallowUnknownFields()
if err := dec.Decode((*issuerCertIDAlias)(i)); err != nil {
return err
}
return i.checkAllSet()
}

func (i *IssuerCertID) checkAllSet() error {
switch {
case i.IA.IsWildcard():
return ErrIssuerIANotSet
case i.CertificateVersion == 0:
return ErrIssuerCertificateVersionNotSet
}
return nil
}

const TypeASJSON = "AS"

// TypeAS indicates an AS certificate.
type TypeAS struct{}

// UnmarshalText checks that the certificate type matches.
func (TypeAS) UnmarshalText(b []byte) error {
if TypeASJSON != string(b) {
return common.NewBasicError(InvalidCertificateType, nil,
"expected", TypeASJSON, "actual", string(b))
}
return nil
}

// MarshalText returns the AS certificate type.
func (TypeAS) MarshalText() ([]byte, error) {
return []byte(TypeASJSON), nil
}
212 changes: 212 additions & 0 deletions go/lib/scrypto/cert/v2/as_json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// Copyright 2019 Anapaya Systems
//
// 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 cert_test

import (
"encoding/json"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/scionproto/scion/go/lib/scrypto"
"github.com/scionproto/scion/go/lib/scrypto/cert/v2"
)

func TestASUnmarshalJSON(t *testing.T) {
tests := map[string]struct {
Modify func(*genCert)
ExpectedErrMsg string
}{
// The valid case is already tested in TestBaseUnmarshalJSON

"Invalid CertificateType": {
Modify: func(g *genCert) {
g.CertificateType = "Issuer"
},
ExpectedErrMsg: cert.InvalidCertificateType,
},
"Missing Issuer.IA": {
Modify: func(g *genCert) {
delete(*g.Issuer, "IA")
},
ExpectedErrMsg: cert.ErrIssuerIANotSet.Error(),
},
"Missing Issuer.CertificateVersion": {
Modify: func(g *genCert) {
delete(*g.Issuer, "CertificateVersion")
},
ExpectedErrMsg: cert.ErrIssuerCertificateVersionNotSet.Error(),
},
"Unknown Issuer field": {
Modify: func(g *genCert) {
(*g.Issuer)["UNKNOWN"] = true
},
ExpectedErrMsg: `json: unknown field "UNKNOWN"`,
},
}
for name, test := range tests {
t.Run("AS certificate: "+name, func(t *testing.T) {
g := newGenASCert()
test.Modify(g)
b, err := json.Marshal(g)
require.NoError(t, err)
err = json.Unmarshal(b, &cert.AS{})
require.Error(t, err)
assert.Contains(t, err.Error(), test.ExpectedErrMsg)
})
}
}

func TestIssuerCertIDUnmarshalJSON(t *testing.T) {
tests := map[string]struct {
Input string
ID cert.IssuerCertID
ExpectedErrMsg string
}{
"Valid": {
Input: `
{
"IA": "1-ff00:0:110",
"CertificateVersion": 2
}
`,
ID: cert.IssuerCertID{
IA: ia110,
CertificateVersion: 2,
},
},
"IA not set": {
Input: `
{
"CertificateVersion": 2
}
`,
ExpectedErrMsg: cert.ErrIssuerIANotSet.Error(),
},
"CertificateVersion not set": {
Input: `
{
"IA": "1-ff00:0:110"
}
`,
ExpectedErrMsg: cert.ErrIssuerCertificateVersionNotSet.Error(),
},
"Unknown field": {
Input: `
{
"IA": "1-ff00:0:110",
"CertificateVersion": 2,
"UNKNOWN": true
}
`,
ExpectedErrMsg: `json: unknown field "UNKNOWN"`,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
var id cert.IssuerCertID
err := json.Unmarshal([]byte(test.Input), &id)
if test.ExpectedErrMsg == "" {
require.NoError(t, err)
assert.Equal(t, test.ID, id)
} else {
require.Error(t, err)
assert.Contains(t, err.Error(), test.ExpectedErrMsg)
}
})
}
}

func TestTypeASUnmarshalJSON(t *testing.T) {
tests := map[string]struct {
Input string
Assert assert.ErrorAssertionFunc
}{
"Valid": {
Input: `"AS"`,
Assert: assert.NoError,
},
"Wrong case": {
Input: `"as"`,
Assert: assert.Error,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
var typeAS cert.TypeAS
test.Assert(t, json.Unmarshal([]byte(test.Input), &typeAS))
})
}
}

func TestTypeASMarshalJSON(t *testing.T) {
var obj struct {
CertificateType cert.TypeAS
}
b, err := json.Marshal(obj)
require.NoError(t, err)
assert.NoError(t, json.Unmarshal(b, &obj))
}

func TestTypeASMarshalSameASString(t *testing.T) {
b, err := json.Marshal(cert.TypeAS{})
require.NoError(t, err)
assert.Equal(t, cert.TypeASJSON, strings.Trim(string(b), `"`))
}

func newGenASCert() *genCert {
c := newASCert()
g := &genCert{
Subject: &c.Subject,
Version: &c.Version,
FormatVersion: &c.FormatVersion,
Description: &c.Description,
OptDistPoints: &c.OptionalDistributionPoints,
Validity: c.Validity,
Keys: &c.Keys,
CertificateType: cert.TypeASJSON,
}
g.Issuer = &map[string]interface{}{
"IA": c.Issuer.IA,
"CertificateVersion": c.Issuer.CertificateVersion,
}
return g
}

func newASCert() cert.AS {
c := cert.AS{
Base: newBaseCert(),
Issuer: cert.IssuerCertID{
IA: ia110,
CertificateVersion: 2,
},
}
c.Keys = map[cert.KeyType]scrypto.KeyMeta{
cert.SigningKey: {
KeyVersion: 1,
Algorithm: scrypto.Ed25519,
Key: []byte{0, 110, 1},
},
cert.EncryptionKey: {
KeyVersion: 1,
Algorithm: scrypto.Ed25519,
Key: []byte{1, 110, 1},
},
}
c.Version = 4
return c
}
Loading

0 comments on commit 3dbd808

Please sign in to comment.