Jan Dittberner
51afebf2c1
- add unit tests for all handwritten code in messages package - use uuid.NewString() instead of uuid.NewUUID() to avoid unnecessary error handling - sort code in messages.go to put type related code close to each other - move checkFailed from hsm.Access.Healthy method to messages.CertificateInfoFailed - add typing for Status field of messages.CertificateInfo
608 lines
15 KiB
Go
608 lines
15 KiB
Go
/*
|
|
Copyright 2022 CAcert Inc.
|
|
SPDX-License-Identifier: Apache-2.0
|
|
|
|
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 messages
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestCommandCode_String(t *testing.T) {
|
|
goodVariants := []struct {
|
|
Name string
|
|
Value CommandCode
|
|
}{
|
|
{"undefined", CmdUndef},
|
|
{"health", CmdHealth},
|
|
{"fetch-crl", CmdFetchCRL},
|
|
}
|
|
|
|
for _, v := range goodVariants {
|
|
t.Run(v.Name, func(t *testing.T) {
|
|
str := v.Value.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
})
|
|
}
|
|
|
|
t.Run("unknown", func(t *testing.T) {
|
|
var unknown CommandCode = -1
|
|
|
|
str := unknown.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.Equal(t, "unknown -1", str)
|
|
})
|
|
}
|
|
|
|
func TestResponseCode_String(t *testing.T) {
|
|
goodVariants := []struct {
|
|
Name string
|
|
Value ResponseCode
|
|
}{
|
|
{"undefined", RespUndef},
|
|
{"error", RespError},
|
|
{"health", RespHealth},
|
|
{"fetch-crl", RespFetchCRL},
|
|
}
|
|
|
|
for _, v := range goodVariants {
|
|
t.Run(v.Name, func(t *testing.T) {
|
|
str := v.Value.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
})
|
|
}
|
|
|
|
t.Run("unknown", func(t *testing.T) {
|
|
var unknown ResponseCode = -2
|
|
|
|
str := unknown.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.Equal(t, "unknown -2", str)
|
|
})
|
|
}
|
|
|
|
func TestBuildCommandAnnounce(t *testing.T) {
|
|
commands := []CommandCode{
|
|
CmdUndef,
|
|
CmdHealth,
|
|
CmdFetchCRL,
|
|
}
|
|
|
|
for _, c := range commands {
|
|
t.Run(c.String(), func(t *testing.T) {
|
|
announce := BuildCommandAnnounce(c)
|
|
|
|
require.NotNil(t, announce)
|
|
|
|
assert.Equal(t, c, announce.Code)
|
|
assert.NotEmpty(t, announce.ID)
|
|
assert.NotEmpty(t, announce.Created)
|
|
assert.True(t, announce.Created.Before(time.Now().UTC()))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommandAnnounce_String(t *testing.T) {
|
|
announce := BuildCommandAnnounce(CmdUndef)
|
|
|
|
require.NotNil(t, announce)
|
|
|
|
str := announce.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.Contains(t, str, "code")
|
|
assert.Contains(t, str, "id")
|
|
assert.Contains(t, str, "created")
|
|
}
|
|
|
|
func TestBuildResponseAnnounce(t *testing.T) {
|
|
responses := []ResponseCode{
|
|
RespError,
|
|
RespUndef,
|
|
RespHealth,
|
|
RespFetchCRL,
|
|
}
|
|
|
|
for _, r := range responses {
|
|
commandID := uuid.NewString()
|
|
|
|
t.Run(r.String(), func(t *testing.T) {
|
|
announce := BuildResponseAnnounce(r, commandID)
|
|
|
|
assert.NotNil(t, announce)
|
|
|
|
assert.Equal(t, r, announce.Code)
|
|
assert.Equal(t, commandID, announce.ID)
|
|
assert.NotEmpty(t, announce.ID)
|
|
assert.NotEmpty(t, announce.Created)
|
|
assert.True(t, announce.Created.Before(time.Now().UTC()))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResponseAnnounce_String(t *testing.T) {
|
|
announce := BuildResponseAnnounce(RespUndef, uuid.NewString())
|
|
|
|
require.NotNil(t, announce)
|
|
|
|
str := announce.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.Contains(t, str, "code")
|
|
assert.Contains(t, str, "id")
|
|
assert.Contains(t, str, "created")
|
|
}
|
|
|
|
func TestHealthCommand_String(t *testing.T) {
|
|
command := &HealthCommand{}
|
|
|
|
str := command.String()
|
|
|
|
assert.Equal(t, "", str)
|
|
}
|
|
|
|
func TestFetchCRLCommand_String(t *testing.T) {
|
|
t.Run("no-last-known", func(t *testing.T) {
|
|
cmd := &FetchCRLCommand{IssuerID: "issuer"}
|
|
|
|
str := cmd.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.Contains(t, str, "issuerId")
|
|
assert.NotContains(t, str, "lastKnownId")
|
|
assert.NotContains(t, str, "0x")
|
|
})
|
|
|
|
t.Run("with-last-known", func(t *testing.T) {
|
|
cmd := &FetchCRLCommand{
|
|
IssuerID: "issuer",
|
|
LastKnownID: big.NewInt(255).Bytes(),
|
|
}
|
|
|
|
str := cmd.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.Contains(t, str, "issuerId")
|
|
assert.Contains(t, str, "lastKnownId")
|
|
assert.Contains(t, str, "0xff")
|
|
})
|
|
}
|
|
|
|
func TestProfileUsage_String(t *testing.T) {
|
|
okValues := []struct {
|
|
Name string
|
|
Usage ProfileUsage
|
|
}{
|
|
{"invalid", UsageInvalid},
|
|
{"ocsp", UsageOCSP},
|
|
{"client", UsageClient},
|
|
{"code", UsageCode},
|
|
{"person", UsagePerson},
|
|
{"server", UsageServer},
|
|
{"server-client", UsageServerClient},
|
|
{"org-client", UsageOrgClient},
|
|
{"org-code", UsageOrgCode},
|
|
{"org-email", UsageOrgEmail},
|
|
{"org-person", UsageOrgPerson},
|
|
{"org-server", UsageOrgServer},
|
|
{"org-server-client", UsageOrgServerClient},
|
|
}
|
|
|
|
for _, v := range okValues {
|
|
t.Run(v.Name, func(t *testing.T) {
|
|
str := v.Usage.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.NotContains(t, str, "unknown profile usage")
|
|
})
|
|
}
|
|
|
|
t.Run("undefined", func(t *testing.T) {
|
|
str := ProfileUsage(255).String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.Contains(t, str, "unknown profile usage")
|
|
assert.Contains(t, str, "255")
|
|
})
|
|
}
|
|
|
|
func TestProfileUsage_Description(t *testing.T) {
|
|
okValues := []struct {
|
|
Name string
|
|
Usage ProfileUsage
|
|
}{
|
|
{"invalid", UsageInvalid},
|
|
{"ocsp", UsageOCSP},
|
|
{"client", UsageClient},
|
|
{"code", UsageCode},
|
|
{"person", UsagePerson},
|
|
{"server", UsageServer},
|
|
{"server-client", UsageServerClient},
|
|
{"org-client", UsageOrgClient},
|
|
{"org-code", UsageOrgCode},
|
|
{"org-email", UsageOrgEmail},
|
|
{"org-person", UsageOrgPerson},
|
|
{"org-server", UsageOrgServer},
|
|
{"org-server-client", UsageOrgServerClient},
|
|
}
|
|
|
|
for _, v := range okValues {
|
|
t.Run(v.Name, func(t *testing.T) {
|
|
str := v.Usage.Description()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.NotContains(t, str, "unknown profile usage")
|
|
assert.Greater(t, len(str), len(v.Name))
|
|
})
|
|
}
|
|
|
|
t.Run("undefined", func(t *testing.T) {
|
|
str := ProfileUsage(255).Description()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.Contains(t, str, "unknown profile usage")
|
|
assert.Contains(t, str, "255")
|
|
})
|
|
}
|
|
|
|
func TestParseUsage(t *testing.T) {
|
|
okValues := []string{
|
|
"ocsp", "client", "code", "person", "server", "server_client",
|
|
"org_client", "org_code", "org_email", "org_person", "org_server", "org_server_client",
|
|
}
|
|
|
|
for _, v := range okValues {
|
|
t.Run(v, func(t *testing.T) {
|
|
u, err := ParseUsage(v)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Greater(t, u, ProfileUsage(0))
|
|
})
|
|
}
|
|
|
|
t.Run("invalid", func(t *testing.T) {
|
|
u, err := ParseUsage("foo")
|
|
|
|
assert.Error(t, err)
|
|
assert.Equal(t, u, UsageInvalid)
|
|
})
|
|
}
|
|
|
|
func TestCAProfile_String(t *testing.T) {
|
|
profiles := []CAProfile{
|
|
{Name: "test-client", UseFor: UsageClient},
|
|
{Name: "test-code", UseFor: UsageCode},
|
|
{Name: "test-ocsp", UseFor: UsageOCSP},
|
|
{Name: "test-org-client", UseFor: UsageOrgClient},
|
|
{Name: "test-org-code", UseFor: UsageOrgCode},
|
|
{Name: "test-org-email", UseFor: UsageOrgEmail},
|
|
{Name: "test-org-person", UseFor: UsageOrgPerson},
|
|
{Name: "test-org-server", UseFor: UsageOrgServer},
|
|
{Name: "test-org-server-client", UseFor: UsageOrgServerClient},
|
|
{Name: "test-person", UseFor: UsagePerson},
|
|
{Name: "test-server", UseFor: UsageServer},
|
|
{Name: "test-server-client", UseFor: UsageServerClient},
|
|
}
|
|
|
|
for _, p := range profiles {
|
|
t.Run(p.Name, func(t *testing.T) {
|
|
str := p.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.True(t, strings.HasPrefix(str, "profile['"))
|
|
assert.True(t, strings.HasSuffix(str, "']"))
|
|
assert.Contains(t, str, "': '")
|
|
assert.Contains(t, str, p.Name)
|
|
assert.Contains(t, str, p.UseFor.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCertificateInfo_String(t *testing.T) {
|
|
infoInstances := []struct {
|
|
Name string
|
|
info CertificateInfo
|
|
}{
|
|
{"failed", CertificateInfoFailed},
|
|
{"ok",
|
|
CertificateInfo{
|
|
Status: CertStatusOk,
|
|
Signing: true,
|
|
Profiles: []CAProfile{{
|
|
Name: "test",
|
|
UseFor: UsageServer,
|
|
}},
|
|
ValidUntil: time.Now().UTC(),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, i := range infoInstances {
|
|
t.Run(i.Name, func(t *testing.T) {
|
|
str := i.info.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.Contains(t, str, fmt.Sprintf("%t", i.info.Signing))
|
|
assert.True(t, strings.HasPrefix(str, "{"))
|
|
assert.True(t, strings.HasSuffix(str, "}"))
|
|
assert.Contains(t, str, "status")
|
|
assert.Contains(t, str, "signing")
|
|
|
|
if len(i.info.Profiles) == 0 {
|
|
assert.NotContains(t, str, "profiles")
|
|
} else {
|
|
assert.Contains(t, str, "profiles")
|
|
}
|
|
|
|
if i.info.ValidUntil.IsZero() {
|
|
assert.Contains(t, str, `"valid-until":"0001-01-01T00:00:00Z"`)
|
|
} else {
|
|
assert.Contains(t, str, "valid-until")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseCertificateInfo(t *testing.T) {
|
|
brokenVariants := []struct{ Name, Value string }{
|
|
{"no json", "no json for you"},
|
|
{"invalid-status", `{"status":"foo"`},
|
|
{"invalid-date", `{"status":"failed","valid-until":"foo"`},
|
|
}
|
|
|
|
for _, v := range brokenVariants {
|
|
t.Run(v.Name, func(t *testing.T) {
|
|
i, err := ParseCertificateInfo(v.Value)
|
|
|
|
assert.Error(t, err)
|
|
assert.Nil(t, i)
|
|
})
|
|
}
|
|
|
|
okVariants := []struct{ Name, Value string }{
|
|
{"minimal", `{"status":"failed"}`},
|
|
{"signing", `{"status":"ok","signing":true}`},
|
|
{"with-date", `{"status":"ok","signing":false,"valid-until":"2022-12-31T23:59:59+01:00"}`},
|
|
}
|
|
|
|
for _, v := range okVariants {
|
|
t.Run(v.Name, func(t *testing.T) {
|
|
i, err := ParseCertificateInfo(v.Value)
|
|
|
|
assert.NoError(t, err)
|
|
require.NotNil(t, i)
|
|
assert.NotEmpty(t, i.Status)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHealthInfo_String(t *testing.T) {
|
|
t.Run("no-more-info", func(t *testing.T) {
|
|
i := &HealthInfo{Source: "test1", Healthy: true}
|
|
|
|
str := i.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.Contains(t, str, "source")
|
|
assert.Contains(t, str, "healthy")
|
|
assert.Contains(t, str, "true")
|
|
assert.NotContains(t, str, "[")
|
|
assert.NotContains(t, str, "]")
|
|
})
|
|
|
|
t.Run("with-more-info", func(t *testing.T) {
|
|
i := &HealthInfo{Source: "test2", Healthy: false, MoreInfo: map[string]string{"foo": "bar", "baz": "boo"}}
|
|
|
|
str := i.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.Contains(t, str, "source")
|
|
assert.Contains(t, str, "healthy")
|
|
assert.Contains(t, str, "false")
|
|
assert.Contains(t, str, "['baz': 'boo', 'foo': 'bar']")
|
|
})
|
|
}
|
|
|
|
func TestHealthResponse_String(t *testing.T) {
|
|
t.Run("no-data", func(t *testing.T) {
|
|
r := &HealthResponse{Version: "foo", Healthy: true}
|
|
|
|
str := r.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.Contains(t, str, "signer version")
|
|
assert.Contains(t, str, "foo")
|
|
assert.Contains(t, str, "healthy")
|
|
assert.Contains(t, str, "true")
|
|
assert.Contains(t, str, "[]")
|
|
})
|
|
|
|
t.Run("with-data", func(t *testing.T) {
|
|
r := &HealthResponse{
|
|
Version: "foo",
|
|
Healthy: false,
|
|
Info: []*HealthInfo{
|
|
{Source: "bar", Healthy: true},
|
|
{Source: "baz", Healthy: false, MoreInfo: map[string]string{
|
|
"alice": "good",
|
|
"malory": "bad",
|
|
}},
|
|
},
|
|
}
|
|
|
|
str := r.String()
|
|
assert.NotEmpty(t, str)
|
|
assert.Contains(t, str, "signer version")
|
|
assert.Contains(t, str, "foo")
|
|
assert.Contains(t, str, "healthy")
|
|
assert.Contains(t, str, "false")
|
|
assert.NotContains(t, str, "[]")
|
|
assert.Contains(t, str, "bar")
|
|
assert.Contains(t, str, "baz")
|
|
assert.Contains(t, str, "alice")
|
|
assert.Contains(t, str, "good")
|
|
assert.Contains(t, str, "malory")
|
|
assert.Contains(t, str, "bad")
|
|
})
|
|
}
|
|
|
|
func TestFetchCRLResponse_String(t *testing.T) {
|
|
t.Run("unchanged", func(t *testing.T) {
|
|
r := &FetchCRLResponse{
|
|
IssuerID: "deep-thought",
|
|
IsDelta: false,
|
|
UnChanged: true,
|
|
CRLNumber: big.NewInt(0x42).Bytes(),
|
|
}
|
|
|
|
str := r.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.Contains(t, str, "issuer id")
|
|
assert.Contains(t, str, "delta=false")
|
|
assert.Contains(t, str, "unchanged=true")
|
|
assert.Contains(t, str, "CRL number=0x42")
|
|
assert.NotContains(t, str, "delta CRL data")
|
|
assert.NotContains(t, str, "CERTIFICATE REVOCATION LIST")
|
|
})
|
|
|
|
t.Run("delta", func(t *testing.T) {
|
|
randomBytes := make([]byte, 100)
|
|
_, _ = rand.Read(randomBytes)
|
|
|
|
r := &FetchCRLResponse{
|
|
IssuerID: "deep-thought",
|
|
IsDelta: true,
|
|
UnChanged: false,
|
|
CRLData: randomBytes,
|
|
CRLNumber: big.NewInt(0x42).Bytes(),
|
|
}
|
|
|
|
str := r.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.Contains(t, str, "issuer id")
|
|
assert.Contains(t, str, "delta=true")
|
|
assert.Contains(t, str, "unchanged=false")
|
|
assert.Contains(t, str, "CRL number=0x42")
|
|
assert.Contains(t, str, "delta CRL data")
|
|
assert.Contains(t, str, "100 bytes")
|
|
assert.NotContains(t, str, "CERTIFICATE REVOCATION LIST")
|
|
})
|
|
|
|
t.Run("bad-crl", func(t *testing.T) {
|
|
randomBytes := make([]byte, 100)
|
|
_, _ = rand.Read(randomBytes)
|
|
|
|
r := &FetchCRLResponse{
|
|
IssuerID: "deep-thought",
|
|
IsDelta: false,
|
|
UnChanged: false,
|
|
CRLData: randomBytes,
|
|
CRLNumber: big.NewInt(0x42).Bytes(),
|
|
}
|
|
|
|
str := r.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.Contains(t, str, "issuer id")
|
|
assert.Contains(t, str, "delta=false")
|
|
assert.Contains(t, str, "unchanged=false")
|
|
assert.Contains(t, str, "CRL number=0x42")
|
|
assert.NotContains(t, str, "delta CRL data")
|
|
assert.NotContains(t, str, "CERTIFICATE REVOCATION LIST")
|
|
assert.Contains(t, str, "could not parse CRL")
|
|
})
|
|
|
|
t.Run("good-crl", func(t *testing.T) {
|
|
keyPair, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
|
|
require.NoError(t, err)
|
|
|
|
certTemplate := &x509.Certificate{
|
|
BasicConstraintsValid: true,
|
|
IsCA: true,
|
|
Issuer: pkix.Name{Country: []string{"un"}, Organization: []string{"Acme Ltd."}, CommonName: "foo CA"},
|
|
KeyUsage: x509.KeyUsageCRLSign | x509.KeyUsageCertSign,
|
|
MaxPathLen: 0,
|
|
MaxPathLenZero: false,
|
|
NotAfter: time.Now().AddDate(5, 0, 0),
|
|
NotBefore: time.Now(),
|
|
SerialNumber: big.NewInt(1),
|
|
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
|
Subject: pkix.Name{Country: []string{"un"}, Organization: []string{"Acme Ltd."}, CommonName: "foo CA"},
|
|
}
|
|
|
|
certBytes, err := x509.CreateCertificate(rand.Reader, certTemplate, certTemplate, keyPair.Public(), keyPair)
|
|
|
|
require.NoError(t, err)
|
|
|
|
cert, _ := x509.ParseCertificate(certBytes)
|
|
|
|
list, _ := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
|
|
Number: big.NewInt(0x42),
|
|
Issuer: cert.Subject,
|
|
ThisUpdate: time.Now(),
|
|
NextUpdate: time.Now().Add(1 * time.Hour),
|
|
}, cert, keyPair)
|
|
|
|
r := &FetchCRLResponse{
|
|
IssuerID: "deep-thought",
|
|
IsDelta: false,
|
|
UnChanged: false,
|
|
CRLData: list,
|
|
CRLNumber: big.NewInt(0x42).Bytes(),
|
|
}
|
|
|
|
str := r.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.Contains(t, str, "issuer id")
|
|
assert.Contains(t, str, "delta=false")
|
|
assert.Contains(t, str, "unchanged=false")
|
|
assert.Contains(t, str, "CRL number=0x42")
|
|
assert.NotContains(t, str, "delta CRL data")
|
|
assert.Contains(t, str, "CERTIFICATE REVOCATION LIST")
|
|
assert.NotContains(t, str, "could not parse CRL")
|
|
})
|
|
}
|
|
|
|
func TestErrorResponse_String(t *testing.T) {
|
|
r := &ErrorResponse{Message: "kaput"}
|
|
|
|
str := r.String()
|
|
|
|
assert.NotEmpty(t, str)
|
|
assert.Contains(t, str, "message")
|
|
assert.Contains(t, str, "kaput")
|
|
}
|