Files
DDI/internal/agentpki/agentpki_test.go
T
beug.k@beugsiud.deandClaude Opus 5.5 e843502cbf feat: M3 contracts for agents, revisions and bundles
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>
2026-10-13 14:54:30 +02:00

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)
}
}
}