Shared Apache-2.0 packages for the signed bundle format and the agent certificate conventions, the agent role interface, REST API spec for agents, enrollment tokens, revisions, deployments and leases, and ADR 0021. Generator settings now hold the target paths on the managed hosts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
132 lines
4.6 KiB
Go
132 lines
4.6 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package agentpki
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"math/big"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const testID = "0b6f3c1e-5d2a-4c8e-9f10-123456789abc"
|
|
|
|
func newCA(t *testing.T) (*x509.Certificate, *ecdsa.PrivateKey) {
|
|
t.Helper()
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tmpl := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "test CA"},
|
|
NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour),
|
|
IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign,
|
|
}
|
|
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cert, _ := x509.ParseCertificate(der)
|
|
return cert, key
|
|
}
|
|
|
|
func issue(t *testing.T, ca *x509.Certificate, caKey *ecdsa.PrivateKey, tmpl *x509.Certificate) *x509.Certificate {
|
|
t.Helper()
|
|
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
tmpl.SerialNumber = big.NewInt(2)
|
|
tmpl.NotBefore = time.Now().Add(-time.Hour)
|
|
tmpl.NotAfter = time.Now().Add(time.Hour)
|
|
der, err := x509.CreateCertificate(rand.Reader, tmpl, ca, &key.PublicKey, caKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cert, _ := x509.ParseCertificate(der)
|
|
return cert
|
|
}
|
|
|
|
func TestAgentID(t *testing.T) {
|
|
if got := AgentURI(testID).String(); got != "urn:ddi-suite:agent:"+testID {
|
|
t.Fatalf("AgentURI = %s", got)
|
|
}
|
|
ca, key := newCA(t)
|
|
good := issue(t, ca, key, &x509.Certificate{Subject: pkix.Name{CommonName: testID}, URIs: []*url.URL{AgentURI(testID)}})
|
|
if id, err := AgentID(good); err != nil || id != testID {
|
|
t.Fatalf("AgentID = %q, %v", id, err)
|
|
}
|
|
other := "11111111-2222-4333-8444-555555555555"
|
|
bad := map[string]*x509.Certificate{
|
|
"none": issue(t, ca, key, &x509.Certificate{Subject: pkix.Name{CommonName: testID}}),
|
|
"cn": issue(t, ca, key, &x509.Certificate{Subject: pkix.Name{CommonName: other}, URIs: []*url.URL{AgentURI(testID)}}),
|
|
"two": issue(t, ca, key, &x509.Certificate{Subject: pkix.Name{CommonName: testID}, URIs: []*url.URL{AgentURI(testID), AgentURI(other)}}),
|
|
"invalid": issue(t, ca, key, &x509.Certificate{Subject: pkix.Name{CommonName: "X"}, URIs: []*url.URL{AgentURI("X")}}),
|
|
"uppercase": issue(t, ca, key, &x509.Certificate{Subject: pkix.Name{CommonName: strings.ToUpper(testID)},
|
|
URIs: []*url.URL{AgentURI(strings.ToUpper(testID))}}),
|
|
}
|
|
for name, c := range bad {
|
|
if _, err := AgentID(c); err == nil {
|
|
t.Errorf("%s: accepted", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFingerprint(t *testing.T) {
|
|
ca, _ := newCA(t)
|
|
fp := Fingerprint(ca)
|
|
if !strings.HasPrefix(fp, "sha256:") || len(fp) != 7+64 {
|
|
t.Fatalf("fingerprint %q", fp)
|
|
}
|
|
if got, err := ParseFingerprint(" " + strings.ToUpper(fp[7:]) + " "); err == nil || got != "" {
|
|
t.Errorf("accepted fingerprint without prefix: %q", got)
|
|
}
|
|
if got, err := ParseFingerprint("sha256:" + strings.ToUpper(fp[7:])); err != nil || got != fp {
|
|
t.Errorf("ParseFingerprint = %q, %v", got, err)
|
|
}
|
|
for _, s := range []string{"sha256:", "sha256:abc", "sha256:" + strings.Repeat("z", 64), "md5:" + fp[7:]} {
|
|
if _, err := ParseFingerprint(s); err == nil {
|
|
t.Errorf("%q accepted", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVerifyChainWithFingerprint(t *testing.T) {
|
|
ca, key := newCA(t)
|
|
otherCA, otherKey := newCA(t)
|
|
leaf := issue(t, ca, key, &x509.Certificate{
|
|
Subject: pkix.Name{CommonName: "ddi.example.test"}, DNSNames: []string{"ddi.example.test"},
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
})
|
|
foreign := issue(t, otherCA, otherKey, &x509.Certificate{
|
|
Subject: pkix.Name{CommonName: "ddi.example.test"}, DNSNames: []string{"ddi.example.test"},
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
})
|
|
fp := Fingerprint(ca)
|
|
|
|
got, err := VerifyChainWithFingerprint([][]byte{leaf.Raw, ca.Raw}, fp, "ddi.example.test")
|
|
if err != nil || !got.Equal(ca) {
|
|
t.Fatalf("valid chain: %v", err)
|
|
}
|
|
cases := map[string]struct {
|
|
raw [][]byte
|
|
fp string
|
|
name string
|
|
}{
|
|
"leaf only": {[][]byte{leaf.Raw}, fp, "ddi.example.test"},
|
|
"wrong CA": {[][]byte{leaf.Raw, otherCA.Raw}, fp, "ddi.example.test"},
|
|
"leaf of other": {[][]byte{foreign.Raw, ca.Raw}, fp, "ddi.example.test"},
|
|
"wrong name": {[][]byte{leaf.Raw, ca.Raw}, fp, "evil.example.test"},
|
|
"bad fingerprint": {[][]byte{leaf.Raw, ca.Raw}, "sha256:00", "ddi.example.test"},
|
|
"garbage": {[][]byte{leaf.Raw, []byte("x")}, fp, "ddi.example.test"},
|
|
}
|
|
for name, c := range cases {
|
|
if _, err := VerifyChainWithFingerprint(c.raw, c.fp, c.name); err == nil {
|
|
t.Errorf("%s: accepted", name)
|
|
}
|
|
}
|
|
}
|