Let printer-certificate pin several fingerprints, since the printer serves an ECDSA and an RSA certificate (issue 1503)
mesh/merge-gate head 6312bc421964 merged into trunk c294f7bd16dc — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head 6312bc421964 merged into trunk c294f7bd16dc — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/merge-gate head 6312bc421964 merged into trunk c294f7bd16dc — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head 6312bc421964 merged into trunk c294f7bd16dc — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
This commit is contained in:
@@ -19,19 +19,36 @@ func Fingerprint(der []byte) string {
|
||||
return strings.Join(parts, ":")
|
||||
}
|
||||
|
||||
// pinned is the check on the certificate a printer serves: the leaf's fingerprint must be the one pinned, and
|
||||
// any other certificate is refused, with both fingerprints in the message. It runs in the handshake, so a
|
||||
// refused certificate is refused before one byte of a request (a password) is sent.
|
||||
func pinned(want string) func([][]byte, [][]*x509.Certificate) error {
|
||||
want = strings.ToUpper(strings.TrimSpace(want))
|
||||
// pinList splits the config value printer-certificate into its fingerprints: one or more, separated by commas
|
||||
// with optional spaces, upper-cased. The printer serves two certificates for one name, an ECDSA one and an RSA
|
||||
// one, and which it serves depends on the client, so the operator pins both (issue 1503).
|
||||
func pinList(value string) []string {
|
||||
var out []string
|
||||
for _, p := range strings.Split(value, ",") {
|
||||
if p = strings.ToUpper(strings.TrimSpace(p)); p != "" {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// pinned is the check on the certificate a printer serves: the leaf's fingerprint must be one of those pinned,
|
||||
// and any other certificate is refused, with the served and every pinned fingerprint in the message. It runs in
|
||||
// the handshake, so a refused certificate is refused before one byte of a request (a password) is sent.
|
||||
func pinned(value string) func([][]byte, [][]*x509.Certificate) error {
|
||||
want := pinList(value)
|
||||
list := strings.Join(want, ", ")
|
||||
return func(raw [][]byte, _ [][]*x509.Certificate) error {
|
||||
if len(raw) == 0 {
|
||||
return fmt.Errorf("the printer served no certificate; the pinned printer-certificate is %s", want)
|
||||
return fmt.Errorf("the printer served no certificate; the pinned printer-certificate is %s", list)
|
||||
}
|
||||
if got := Fingerprint(raw[0]); got != want {
|
||||
return fmt.Errorf("the printer's certificate is %s, but the pinned printer-certificate is %s: refused", got, want)
|
||||
got := Fingerprint(raw[0])
|
||||
for _, w := range want {
|
||||
if got == w {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return fmt.Errorf("the printer's certificate is %s, but the pinned printer-certificate is %s: refused", got, list)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Captured from the printer 2026-10-20 (testdata/pin): it serves two self-signed certificates for one name, an
|
||||
// ECDSA one to a client offering ECDSA and an RSA one to a client offering RSA only (issue 1503).
|
||||
const (
|
||||
printerECDSAPrint = "EA:B4:B7:0F:96:CD:A6:2B:61:53:2E:F0:A8:D6:9F:42:EA:9F:66:0A:82:5B:C2:43:0B:97:C5:9A:7A:B4:18:5B"
|
||||
printerRSAPrint = "7A:3D:E2:A4:1E:AB:A9:7B:E6:61:0A:C4:18:DD:68:FF:9B:A5:77:E8:02:7F:7E:F8:97:D0:3B:82:A2:89:7D:F4"
|
||||
)
|
||||
|
||||
func capturedDER(t *testing.T, name string) []byte {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile("testdata/pin/" + name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, _ := pem.Decode(raw)
|
||||
if b == nil {
|
||||
t.Fatalf("%s is not PEM", name)
|
||||
}
|
||||
return b.Bytes
|
||||
}
|
||||
|
||||
// 1503/AC1: the two captured certificates have the two fingerprints, and a pin listing both accepts each.
|
||||
func TestThePrintersTwoCapturedCertificatesAreBothAcceptedByAPinListingBoth(t *testing.T) {
|
||||
both := printerECDSAPrint + ", " + printerRSAPrint
|
||||
for file, want := range map[string]string{"ecdsa.pem": printerECDSAPrint, "rsa.pem": printerRSAPrint} {
|
||||
der := capturedDER(t, file)
|
||||
if got := Fingerprint(der); got != want {
|
||||
t.Fatalf("%s has fingerprint %s, want %s", file, got, want)
|
||||
}
|
||||
if err := pinned(both)([][]byte{der}, nil); err != nil {
|
||||
t.Errorf("%s refused by a pin listing both: %v", file, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1503/AC1: a server presenting one certificate is accepted when the pin lists its fingerprint among two.
|
||||
func TestAPinListingTwoFingerprintsAcceptsTheCertificateTheServerPresents(t *testing.T) {
|
||||
f := newFakePrinter(t, "B1891", "hunter2")
|
||||
a, err := NewAdmin(f.host(), printerECDSAPrint+","+f.print(), "hunter2")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := a.Login(context.Background()); err != nil {
|
||||
t.Fatalf("a pin listing the served certificate was refused: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 1503/AC2: a pin that lists only another certificate still refuses, and names every pinned fingerprint.
|
||||
func TestAPinListingOnlyOtherCertificatesStillRefuses(t *testing.T) {
|
||||
f := newFakePrinter(t, "B1891", "hunter2")
|
||||
a, _ := NewAdmin(f.host(), printerECDSAPrint+" , "+printerRSAPrint, "hunter2")
|
||||
err := a.Login(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("a certificate that is in no pinned fingerprint was accepted")
|
||||
}
|
||||
for _, want := range []string{f.print(), printerECDSAPrint, printerRSAPrint} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("error %q does not name %s", err, want)
|
||||
}
|
||||
}
|
||||
if len(f.requests) != 0 {
|
||||
t.Errorf("the server was sent %v", f.requests)
|
||||
}
|
||||
}
|
||||
|
||||
// 1503/AC1: a single fingerprint is still a valid pin, in either case.
|
||||
func TestASingleFingerprintStillPins(t *testing.T) {
|
||||
der := capturedDER(t, "rsa.pem")
|
||||
for _, p := range []string{printerRSAPrint, strings.ToLower(printerRSAPrint), " " + printerRSAPrint + " "} {
|
||||
if err := pinned(p)([][]byte{der}, nil); err != nil {
|
||||
t.Errorf("pin %q refused: %v", p, err)
|
||||
}
|
||||
}
|
||||
if err := pinned(printerECDSAPrint)([][]byte{der}, nil); err == nil {
|
||||
t.Error("the other certificate's pin accepted the RSA certificate")
|
||||
}
|
||||
}
|
||||
|
||||
// 1503/AC3: the manifest's pattern takes one or more fingerprints, separated by commas and optional spaces, and
|
||||
// nothing else.
|
||||
func TestThePrinterCertificatePatternTakesACommaSeparatedList(t *testing.T) {
|
||||
m := readManifest(t)
|
||||
re := regexp.MustCompile(`^(?:` + m.Config["printer-certificate"]["pattern"].(string) + `)$`)
|
||||
for _, ok := range []string{printerRSAPrint, printerECDSAPrint + "," + printerRSAPrint, printerECDSAPrint + ", " + printerRSAPrint} {
|
||||
if !re.MatchString(ok) {
|
||||
t.Errorf("pattern refuses %q", ok)
|
||||
}
|
||||
}
|
||||
for _, bad := range []string{"", printerRSAPrint + ",", "," + printerRSAPrint, printerECDSAPrint + printerRSAPrint,
|
||||
printerECDSAPrint + ",," + printerRSAPrint, printerECDSAPrint + ";" + printerRSAPrint, printerRSAPrint + ",EAB4", strings.ToLower(printerRSAPrint)} {
|
||||
if re.MatchString(bad) {
|
||||
t.Errorf("pattern admits %q", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBVzCB/qADAgECAgQoWaUiMAoGCCqGSM49BAMCMCAxHjAcBgNVBAMTFUJSVzU4
|
||||
Q0RDOURGQzgwQi5sb2NhbDAgFw0wMDAxMDEwMDAwMDBaGA8yMTEwMTIzMTIzNTk1
|
||||
OVowIDEeMBwGA1UEAxMVQlJXNThDREM5REZDODBCLmxvY2FsMFkwEwYHKoZIzj0C
|
||||
AQYIKoZIzj0DAQcDQgAENrR0q9JKcMSypD7yVGMzXpaarlLA3nc4ju1gh9ymeqjy
|
||||
xyfdiAy08GgvGKZMv+EaqH1xoAaaglmF8Dhgn0GrS6MkMCIwIAYDVR0RBBkwF4IV
|
||||
QlJXNThDREM5REZDODBCLmxvY2FsMAoGCCqGSM49BAMCA0gAMEUCIQCTpBb5+82s
|
||||
f/OXfEA77SJREW+6UHFgaDruuJ06wdTouwIgUUr5FkoGzsunwrYAZmqcBw96UKK5
|
||||
o5tz3ZLrtSke6VM=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,18 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC5DCCAcygAwIBAgIEIvomBDANBgkqhkiG9w0BAQsFADAgMR4wHAYDVQQDExVC
|
||||
Ulc1OENEQzlERkM4MEIubG9jYWwwIBcNMDAwMTAxMDAwMDAwWhgPMjExMDEyMzEy
|
||||
MzU5NTlaMCAxHjAcBgNVBAMTFUJSVzU4Q0RDOURGQzgwQi5sb2NhbDCCASIwDQYJ
|
||||
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMGbBrytUZsYjcH/d75H1ax+EMKElDWX
|
||||
p+bnafu5vB0WooGCG7gYbCGuYJRnI+PtFGEVwPlOQojiGmvuDVO2THs+kefcAi/c
|
||||
7inSrD550kBsfWYVVhyZ3e0tK+UsEgTC9QQ0fdi3AFWnS68L93ooPkCgVBCl1B/c
|
||||
5VsoGOtdFGgDLPJlmf22KxfSjI2+0cB4/f+VJX+1AoP71s1A4NN8okfuEL+mC1h1
|
||||
tbzaT+MLTtgxAFMu//tF3287yVLyG+vSBAbX7pujpBsrzBzv5mn2dZrlPwDjD17w
|
||||
oqfjfuu1ERpxn3fpBtexqXV3ybw2ftGGOVtn9kV2Po/vTTfGMgrz33UCAwEAAaMk
|
||||
MCIwIAYDVR0RBBkwF4IVQlJXNThDREM5REZDODBCLmxvY2FsMA0GCSqGSIb3DQEB
|
||||
CwUAA4IBAQBfP6IfdFsaWyc8FHooiRW/oS9OlZxDUrmUrdNP9doRq7PROM2bCnIZ
|
||||
D6q5B34bLFxHmitMLpy3MKV475yrS83ges5k5rlUPlRFIJaSCpNY6xRwUZdGgurX
|
||||
h8KdHERXmtCuj1egkTJ9SRAchjlgdQevVJRgeFUCdVZ63NVIBy37zcIVBHOB5R3L
|
||||
V9lMijXwV2ow9DnXARdXYeSn98XoOMVNyL8lHESyHPN2cS8fijunudGtoieSA0TK
|
||||
NpAr18jfGBYvM35AYwvSFGqo/yECCfKVHup2HykhOqhWnrvv8OyHmsvdYn2s9bx1
|
||||
lT903TyZyla6PjeA/8dGj4eJHkVIKv87
|
||||
-----END CERTIFICATE-----
|
||||
Reference in New Issue
Block a user