From a0058c74dc620cbf1cad6a38d1d4e11f4aa37755 Mon Sep 17 00:00:00 2001 From: Oncilla Date: Tue, 30 Jul 2019 15:36:17 +0200 Subject: [PATCH] cert: Add AS certificate (#2936) This PR adds the AS certificate with parsing and validation. contributes to #2853 --- go/lib/scrypto/cert/v2/BUILD.bazel | 7 +- go/lib/scrypto/cert/v2/as.go | 151 ++++++++++++++ go/lib/scrypto/cert/v2/as_json_test.go | 254 +++++++++++++++++++++++ go/lib/scrypto/cert/v2/as_test.go | 92 ++++++++ go/lib/scrypto/cert/v2/base.go | 45 +++- go/lib/scrypto/cert/v2/base_json_test.go | 89 ++++++++ 6 files changed, 636 insertions(+), 2 deletions(-) create mode 100644 go/lib/scrypto/cert/v2/as.go create mode 100644 go/lib/scrypto/cert/v2/as_json_test.go create mode 100644 go/lib/scrypto/cert/v2/as_test.go diff --git a/go/lib/scrypto/cert/v2/BUILD.bazel b/go/lib/scrypto/cert/v2/BUILD.bazel index ed8a42ee56..8edef20285 100644 --- a/go/lib/scrypto/cert/v2/BUILD.bazel +++ b/go/lib/scrypto/cert/v2/BUILD.bazel @@ -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 = [ @@ -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", ], diff --git a/go/lib/scrypto/cert/v2/as.go b/go/lib/scrypto/cert/v2/as.go new file mode 100644 index 0000000000..21f49446bf --- /dev/null +++ b/go/lib/scrypto/cert/v2/as.go @@ -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 +} diff --git a/go/lib/scrypto/cert/v2/as_json_test.go b/go/lib/scrypto/cert/v2/as_json_test.go new file mode 100644 index 0000000000..048b7faaab --- /dev/null +++ b/go/lib/scrypto/cert/v2/as_json_test.go @@ -0,0 +1,254 @@ +// 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" +) + +type asTest struct { + baseTest + ModifyExpected func(*cert.AS) +} + +func TestASUnmarshalJSON(t *testing.T) { + tests := map[string]asTest{ + "With revocation key": { + baseTest: baseTest{ + Modify: func(g *genCert) { + (*g.Keys)[cert.RevocationKey] = scrypto.KeyMeta{ + KeyVersion: 1, + Algorithm: scrypto.Ed25519, + Key: []byte{2, 110, 1}, + } + }, + }, + ModifyExpected: func(c *cert.AS) { + c.Keys[cert.RevocationKey] = scrypto.KeyMeta{ + KeyVersion: 1, + Algorithm: scrypto.Ed25519, + Key: []byte{2, 110, 1}, + } + }, + }, + "Invalid CertificateType": { + baseTest: baseTest{ + Modify: func(g *genCert) { + g.CertificateType = "Issuer" + }, + ExpectedErrMsg: cert.InvalidCertificateType, + }, + }, + "Missing Issuer.IA": { + baseTest: baseTest{ + Modify: func(g *genCert) { + delete(*g.Issuer, "IA") + }, + ExpectedErrMsg: cert.ErrIssuerIANotSet.Error(), + }, + }, + "Missing Issuer.CertificateVersion": { + baseTest: baseTest{ + Modify: func(g *genCert) { + delete(*g.Issuer, "CertificateVersion") + }, + ExpectedErrMsg: cert.ErrIssuerCertificateVersionNotSet.Error(), + }, + }, + "Unknown Issuer field": { + baseTest: baseTest{ + Modify: func(g *genCert) { + (*g.Issuer)["UNKNOWN"] = true + }, + ExpectedErrMsg: `json: unknown field "UNKNOWN"`, + }, + }, + } + for name, test := range baseUnmarshalJSONTests() { + if _, ok := tests[name]; ok { + t.Fatalf("Duplicate test name: %s", name) + } + tests[name] = asTest{baseTest: test} + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + g := newGenASCert() + test.Modify(g) + b, err := json.Marshal(g) + require.NoError(t, err) + var as cert.AS + err = json.Unmarshal(b, &as) + if test.ExpectedErrMsg == "" { + require.NoError(t, err) + expected := newASCert() + if test.ModifyExpected != nil { + test.ModifyExpected(&expected) + } + assert.Equal(t, expected, as) + } else { + 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 +} diff --git a/go/lib/scrypto/cert/v2/as_test.go b/go/lib/scrypto/cert/v2/as_test.go new file mode 100644 index 0000000000..518e162216 --- /dev/null +++ b/go/lib/scrypto/cert/v2/as_test.go @@ -0,0 +1,92 @@ +// 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 ( + "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 TestASValidate(t *testing.T) { + tests := map[string]struct { + Modify func(*cert.AS) + ExpectedErrMsg string + }{ + "Valid": { + Modify: func(_ *cert.AS) {}, + }, + "Revocation Key": { + Modify: func(c *cert.AS) { + c.Keys[cert.RevocationKey] = scrypto.KeyMeta{ + KeyVersion: 1, + Algorithm: scrypto.Ed25519, + Key: []byte{3, 110, 1}, + } + }, + }, + "Base Validation error": { + Modify: func(c *cert.AS) { + c.Subject.A = 0 + }, + ExpectedErrMsg: cert.InvalidSubject, + }, + "Issuing Key": { + Modify: func(c *cert.AS) { + c.Keys[cert.IssuingKey] = scrypto.KeyMeta{ + KeyVersion: 1, + Algorithm: scrypto.Ed25519, + Key: []byte{2, 110, 1}, + } + }, + ExpectedErrMsg: cert.UnexpectedKey, + }, + "No SigningKey": { + Modify: func(c *cert.AS) { + delete(c.Keys, cert.SigningKey) + }, + ExpectedErrMsg: cert.MissingKey, + }, + "No EncryptionKey": { + Modify: func(c *cert.AS) { + delete(c.Keys, cert.EncryptionKey) + }, + ExpectedErrMsg: cert.MissingKey, + }, + "Issuer ISD mismatch": { + Modify: func(c *cert.AS) { + c.Issuer.IA.I = c.Subject.I + 1 + }, + ExpectedErrMsg: cert.IssuerDifferentISD, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + c := newASCert() + test.Modify(&c) + err := c.Validate() + if test.ExpectedErrMsg == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + assert.Contains(t, err.Error(), test.ExpectedErrMsg) + } + }) + } +} diff --git a/go/lib/scrypto/cert/v2/base.go b/go/lib/scrypto/cert/v2/base.go index c078b48b5d..97108901c8 100644 --- a/go/lib/scrypto/cert/v2/base.go +++ b/go/lib/scrypto/cert/v2/base.go @@ -33,7 +33,7 @@ const ( UnsupportedFormat = "Unsupported certificate format" ) -// Validation errors +// Validation errors with context. const ( // InvalidValidityPeriod indicates an invalid validity period. InvalidValidityPeriod = "invalid validity period" @@ -47,6 +47,29 @@ const ( MissingKey = "missing key" ) +// Parsing errors. +var ( + // ErrSubjectNotSet indicates Subject is not set. + ErrSubjectNotSet = errors.New("Subject not set") + // ErrVersionNotSet indicates Version is not set. + ErrVersionNotSet = errors.New("Version not set") + // ErrFormatVersionNotSet indicates FormatVersion is not set. + ErrFormatVersionNotSet = errors.New("FormatVersion not set") + // ErrDescriptionNotSet indicates Description is not set. + ErrDescriptionNotSet = errors.New("Description not set") + // ErrOptionalDistributionPointsNotSet indicates OptionalDistributionPoints is not set. + ErrOptionalDistributionPointsNotSet = errors.New("OptionalDistributionPoints not set") + // ErrValidityNotSet indicates Validity is not set. + ErrValidityNotSet = errors.New("Validity not set") + // ErrKeysNotSet indicates Keys is not set. + ErrKeysNotSet = errors.New("Keys not set") + // ErrIssuerNotSet indicates Issuer is not set. + ErrIssuerNotSet = errors.New("Issuer not set") + // ErrCertificateTypeNotSet indicates CertificateType is not set. + ErrCertificateTypeNotSet = errors.New("CertificateType not set") +) + +// Validation errors. var ( // ErrAlgorithmNotSet indicates the key algorithm is not set. ErrAlgorithmNotSet = errors.New("algorithm not set") @@ -122,6 +145,26 @@ func (b *Base) checkKeyExistence(keyType KeyType, shouldExist bool) error { return nil } +func (b *Base) checkAllSet() error { + switch { + case b.Subject.IsZero(): + return ErrSubjectNotSet + case b.Version == 0: + return ErrVersionNotSet + case b.FormatVersion == 0: + return ErrFormatVersionNotSet + case b.Description == "": + return ErrDescriptionNotSet + case b.OptionalDistributionPoints == nil: + return ErrOptionalDistributionPointsNotSet + case b.Validity == nil: + return ErrValidityNotSet + case b.Keys == nil: + return ErrKeysNotSet + } + return nil +} + const ( IssuingKeyJSON = "Issuing" SigningKeyJSON = "Signing" diff --git a/go/lib/scrypto/cert/v2/base_json_test.go b/go/lib/scrypto/cert/v2/base_json_test.go index 53f2c2128a..43d7f81ff1 100644 --- a/go/lib/scrypto/cert/v2/base_json_test.go +++ b/go/lib/scrypto/cert/v2/base_json_test.go @@ -22,9 +22,85 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/scionproto/scion/go/lib/addr" + "github.com/scionproto/scion/go/lib/scrypto" "github.com/scionproto/scion/go/lib/scrypto/cert/v2" ) +type baseTest struct { + Modify func(*genCert) + ExpectedErrMsg string +} + +func baseUnmarshalJSONTests() map[string]baseTest { + tests := map[string]baseTest{ + "Valid": { + Modify: func(_ *genCert) {}, + }, + "Subject not set": { + Modify: func(g *genCert) { + g.Subject = nil + }, + ExpectedErrMsg: cert.ErrSubjectNotSet.Error(), + }, + "Version not set": { + Modify: func(g *genCert) { + g.Version = nil + }, + ExpectedErrMsg: cert.ErrVersionNotSet.Error(), + }, + "FormatVersion not set": { + Modify: func(g *genCert) { + g.FormatVersion = nil + }, + ExpectedErrMsg: cert.ErrFormatVersionNotSet.Error(), + }, + "Description not set": { + Modify: func(g *genCert) { + g.Description = nil + }, + ExpectedErrMsg: cert.ErrDescriptionNotSet.Error(), + }, + "OptionalDistributionPoints not set": { + Modify: func(g *genCert) { + g.OptDistPoints = nil + }, + ExpectedErrMsg: cert.ErrOptionalDistributionPointsNotSet.Error(), + }, + "Validity not set": { + Modify: func(g *genCert) { + g.Validity = nil + }, + ExpectedErrMsg: cert.ErrValidityNotSet.Error(), + }, + "Keys not set": { + Modify: func(g *genCert) { + g.Keys = nil + }, + ExpectedErrMsg: cert.ErrKeysNotSet.Error(), + }, + "Issuer not set": { + Modify: func(g *genCert) { + g.Issuer = nil + }, + ExpectedErrMsg: cert.ErrIssuerNotSet.Error(), + }, + "CertificateType not set": { + Modify: func(g *genCert) { + g.CertificateType = "" + }, + ExpectedErrMsg: cert.ErrCertificateTypeNotSet.Error(), + }, + "Unknown field": { + Modify: func(g *genCert) { + g.UnknownField = "true" + }, + ExpectedErrMsg: `json: unknown field "UNKNOWN"`, + }, + } + return tests +} + func TestKeyTypeUnmarshalJSON(t *testing.T) { tests := map[string]struct { Input string @@ -169,3 +245,16 @@ func TestFormatVersionUnmarshalJSON(t *testing.T) { }) } } + +type genCert struct { + Subject *addr.IA `json:"Subject,omitempty"` + Version *scrypto.Version `json:"Version,omitempty"` + FormatVersion *cert.FormatVersion `json:"FormatVersion,omitempty"` + Description *string `json:"Description,omitempty"` + OptDistPoints *[]addr.IA `json:"OptionalDistributionPoints,omitempty"` + Validity *scrypto.Validity `json:"Validity,omitempty"` + Keys *map[cert.KeyType]scrypto.KeyMeta `json:"Keys,omitempty"` + Issuer *map[string]interface{} `json:"Issuer,omitempty"` + CertificateType string `json:"CertificateType,omitempty"` + UnknownField string `json:"UNKNOWN,omitempty"` +}