Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef5341b706 | ||
|
|
b999374650 | ||
|
|
c247dead11 | ||
|
|
1fda73153f | ||
|
|
1b4c3d2e8c | ||
|
|
b903b535d3 | ||
|
|
459a9db11b | ||
|
|
74e709ad8a | ||
|
|
b3790b8d91 | ||
|
|
1dc4269656 | ||
|
|
97bf787255 | ||
|
|
952517d181 | ||
|
|
c6fce02826 | ||
|
|
0ae49b8145 |
+24
-11
@@ -35,6 +35,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -589,8 +590,9 @@ func (c *Client) TLSSNI02ChallengeCert(token string, opt ...CertOption) (tls.Cer
|
|||||||
|
|
||||||
// TLSALPN01ChallengeCert creates a certificate for TLS-ALPN-01 challenge response.
|
// TLSALPN01ChallengeCert creates a certificate for TLS-ALPN-01 challenge response.
|
||||||
// Servers can present the certificate to validate the challenge and prove control
|
// Servers can present the certificate to validate the challenge and prove control
|
||||||
// over a domain name. For more details on TLS-ALPN-01 see
|
// over an identifier (either a DNS name or the textual form of an IPv4 or IPv6
|
||||||
// https://tools.ietf.org/html/draft-shoemaker-acme-tls-alpn-00#section-3
|
// address). For more details on TLS-ALPN-01 see
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc8737 and https://www.rfc-editor.org/rfc/rfc8738
|
||||||
//
|
//
|
||||||
// The token argument is a Challenge.Token value.
|
// The token argument is a Challenge.Token value.
|
||||||
// If a WithKey option is provided, its private part signs the returned cert,
|
// If a WithKey option is provided, its private part signs the returned cert,
|
||||||
@@ -598,9 +600,13 @@ func (c *Client) TLSSNI02ChallengeCert(token string, opt ...CertOption) (tls.Cer
|
|||||||
// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve.
|
// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve.
|
||||||
//
|
//
|
||||||
// The returned certificate is valid for the next 24 hours and must be presented only when
|
// The returned certificate is valid for the next 24 hours and must be presented only when
|
||||||
// the server name in the TLS ClientHello matches the domain, and the special acme-tls/1 ALPN protocol
|
// the server name in the TLS ClientHello matches the identifier, and the special acme-tls/1 ALPN protocol
|
||||||
// has been specified.
|
// has been specified.
|
||||||
func (c *Client) TLSALPN01ChallengeCert(token, domain string, opt ...CertOption) (cert tls.Certificate, err error) {
|
//
|
||||||
|
// Validation requests for IP address identifiers will use the reverse DNS form in the server name
|
||||||
|
// in the TLS ClientHello since the SNI extension is not supported for IP addresses.
|
||||||
|
// See RFC 8738 Section 6 for more information.
|
||||||
|
func (c *Client) TLSALPN01ChallengeCert(token, identifier string, opt ...CertOption) (cert tls.Certificate, err error) {
|
||||||
ka, err := keyAuth(c.Key.Public(), token)
|
ka, err := keyAuth(c.Key.Public(), token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return tls.Certificate{}, err
|
return tls.Certificate{}, err
|
||||||
@@ -630,7 +636,7 @@ func (c *Client) TLSALPN01ChallengeCert(token, domain string, opt ...CertOption)
|
|||||||
}
|
}
|
||||||
tmpl.ExtraExtensions = append(tmpl.ExtraExtensions, acmeExtension)
|
tmpl.ExtraExtensions = append(tmpl.ExtraExtensions, acmeExtension)
|
||||||
newOpt = append(newOpt, WithTemplate(tmpl))
|
newOpt = append(newOpt, WithTemplate(tmpl))
|
||||||
return tlsChallengeCert([]string{domain}, newOpt)
|
return tlsChallengeCert(identifier, newOpt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// popNonce returns a nonce value previously stored with c.addNonce
|
// popNonce returns a nonce value previously stored with c.addNonce
|
||||||
@@ -749,10 +755,14 @@ func defaultTLSChallengeCertTemplate() *x509.Certificate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// tlsChallengeCert creates a temporary certificate for TLS-ALPN challenges
|
// tlsChallengeCert creates a temporary certificate for TLS-ALPN challenges
|
||||||
// with the given SANs and auto-generated public/private key pair.
|
// for the given identifier, using an auto-generated public/private key pair.
|
||||||
// The Subject Common Name is set to the first SAN to aid debugging.
|
//
|
||||||
|
// If the provided identifier is a domain name, it will be used as a DNS type SAN and for the
|
||||||
|
// subject common name. If the provided identifier is an IP address it will be used as an IP type
|
||||||
|
// SAN.
|
||||||
|
//
|
||||||
// To create a cert with a custom key pair, specify WithKey option.
|
// To create a cert with a custom key pair, specify WithKey option.
|
||||||
func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) {
|
func tlsChallengeCert(identifier string, opt []CertOption) (tls.Certificate, error) {
|
||||||
var key crypto.Signer
|
var key crypto.Signer
|
||||||
tmpl := defaultTLSChallengeCertTemplate()
|
tmpl := defaultTLSChallengeCertTemplate()
|
||||||
for _, o := range opt {
|
for _, o := range opt {
|
||||||
@@ -776,9 +786,12 @@ func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) {
|
|||||||
return tls.Certificate{}, err
|
return tls.Certificate{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tmpl.DNSNames = san
|
|
||||||
if len(san) > 0 {
|
if ip := net.ParseIP(identifier); ip != nil {
|
||||||
tmpl.Subject.CommonName = san[0]
|
tmpl.IPAddresses = []net.IP{ip}
|
||||||
|
} else {
|
||||||
|
tmpl.DNSNames = []string{identifier}
|
||||||
|
tmpl.Subject.CommonName = identifier
|
||||||
}
|
}
|
||||||
|
|
||||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key)
|
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key)
|
||||||
|
|||||||
@@ -0,0 +1,804 @@
|
|||||||
|
// Copyright 2025 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package acme_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/acme"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// pebbleModVersion is the module version used for Pebble and Pebble's
|
||||||
|
// challenge test server. It is ignored if `-pebble-local-dir` is provided.
|
||||||
|
pebbleModVersion = "v2.7.0"
|
||||||
|
// startingPort is the first port number used for binding interface
|
||||||
|
// addresses. Each call to takeNextPort() will increment a port number
|
||||||
|
// starting at this value.
|
||||||
|
startingPort = 5555
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
pebbleLocalDir = flag.String(
|
||||||
|
"pebble-local-dir",
|
||||||
|
"",
|
||||||
|
"Local Pebble to use, instead of fetching from source",
|
||||||
|
)
|
||||||
|
nextPort atomic.Uint32
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
nextPort.Store(startingPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithPebble(t *testing.T) {
|
||||||
|
// We want to use process groups w/ syscall.Kill, and the acme package
|
||||||
|
// is very platform-agnostic, so skip on non-Linux.
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
t.Skip("skipping pebble tests on non-linux OS")
|
||||||
|
}
|
||||||
|
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping pebble tests in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
challSrv func(*environment) (challengeServer, string)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "TLSALPN01-Issuance",
|
||||||
|
challSrv: func(env *environment) (challengeServer, string) {
|
||||||
|
bindAddr := fmt.Sprintf(":%d", env.config.TLSPort)
|
||||||
|
return newChallTLSServer(bindAddr), bindAddr
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "HTTP01-Issuance",
|
||||||
|
challSrv: func(env *environment) (challengeServer, string) {
|
||||||
|
bindAddr := fmt.Sprintf(":%d", env.config.HTTPPort)
|
||||||
|
return newChallHTTPServer(bindAddr), bindAddr
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := startPebbleEnvironment(t, nil)
|
||||||
|
challSrv, challSrvAddr := tt.challSrv(&env)
|
||||||
|
challSrv.Run()
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
challSrv.Shutdown()
|
||||||
|
})
|
||||||
|
|
||||||
|
waitForServer(t, challSrvAddr)
|
||||||
|
testIssuance(t, &env, challSrv)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// challengeServer abstracts over the details of running a challenge response
|
||||||
|
// server for some supported acme.Challenge type. Responses are provisioned
|
||||||
|
// during the test issuance process to be presented to the ACME server's
|
||||||
|
// validation authority.
|
||||||
|
type challengeServer interface {
|
||||||
|
Run()
|
||||||
|
Shutdown() error
|
||||||
|
Supported(chal *acme.Challenge) bool
|
||||||
|
Provision(client *acme.Client, ident acme.AuthzID, chal *acme.Challenge) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// challTLSServer is a simple challenge response server that listens for TLS
|
||||||
|
// connections on a specific port and if they are TLS-ALPN-01 challenge
|
||||||
|
// requests, completes the handshake using the configured challenge response
|
||||||
|
// certificate for the SNI value provided.
|
||||||
|
type challTLSServer struct {
|
||||||
|
*http.Server
|
||||||
|
// mu protects challCerts.
|
||||||
|
mu sync.RWMutex
|
||||||
|
// challCerts is a map from SNI domain name to challenge response certificate.
|
||||||
|
challCerts map[string]*tls.Certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc8737#section-4
|
||||||
|
const acmeTLSAlpnProtocol = "acme-tls/1"
|
||||||
|
|
||||||
|
func newChallTLSServer(address string) *challTLSServer {
|
||||||
|
challServer := &challTLSServer{Server: &http.Server{
|
||||||
|
Addr: address,
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 5 * time.Second,
|
||||||
|
}, challCerts: make(map[string]*tls.Certificate)}
|
||||||
|
|
||||||
|
// Configure the server to support the TLS-ALPN-01 challenge protocol
|
||||||
|
// and to use a callback for selecting the handshake certificate.
|
||||||
|
challServer.Server.TLSConfig = &tls.Config{
|
||||||
|
NextProtos: []string{acmeTLSAlpnProtocol},
|
||||||
|
GetCertificate: challServer.getCertificate,
|
||||||
|
}
|
||||||
|
|
||||||
|
return challServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *challTLSServer) Shutdown() error {
|
||||||
|
log.Printf("challTLSServer: shutting down")
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10)
|
||||||
|
defer cancel()
|
||||||
|
return c.Server.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *challTLSServer) Run() {
|
||||||
|
go func() {
|
||||||
|
// Note: certFile and keyFile are empty because our config uses a
|
||||||
|
// GetCertificate callback.
|
||||||
|
if err := c.Server.ListenAndServeTLS("", ""); err != nil {
|
||||||
|
if !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Printf("challTLSServer error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *challTLSServer) Supported(chal *acme.Challenge) bool {
|
||||||
|
return chal.Type == "tls-alpn-01"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *challTLSServer) Provision(client *acme.Client, ident acme.AuthzID, chal *acme.Challenge) error {
|
||||||
|
respCert, err := client.TLSALPN01ChallengeCert(chal.Token, ident.Value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("challTLSServer: failed to generate challlenge response cert for %s: %w",
|
||||||
|
ident.Value, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("challTLSServer: setting challenge response certificate for %s", ident.Value)
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.challCerts[ident.Value] = &respCert
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *challTLSServer) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
// Verify the request looks like a TLS-ALPN-01 challenge request.
|
||||||
|
if len(clientHello.SupportedProtos) != 1 || clientHello.SupportedProtos[0] != acmeTLSAlpnProtocol {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"challTLSServer: non-TLS-ALPN-01 challenge request received with SupportedProtos: %s",
|
||||||
|
clientHello.SupportedProtos)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverName := clientHello.ServerName
|
||||||
|
|
||||||
|
// TLS-ALPN-01 challenge requests for IP addresses are encoded in the SNI
|
||||||
|
// using the reverse-DNS notation. See RFC 8738 Section 6:
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc8738.html#section-6
|
||||||
|
if strings.HasSuffix(serverName, ".in-addr.arpa") {
|
||||||
|
serverName = strings.TrimSuffix(serverName, ".in-addr.arpa")
|
||||||
|
parts := strings.Split(serverName, ".")
|
||||||
|
for i, j := 0, len(parts)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
parts[i], parts[j] = parts[j], parts[i]
|
||||||
|
}
|
||||||
|
serverName = strings.Join(parts, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("challTLSServer: selecting certificate for request from %s for %s",
|
||||||
|
clientHello.Conn.RemoteAddr(), serverName)
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
cert := c.challCerts[serverName]
|
||||||
|
if cert == nil {
|
||||||
|
return nil, fmt.Errorf("challTLSServer: no challenge response certificate configured for %s", serverName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// challHTTPServer is a simple challenge response server that listens for HTTP
|
||||||
|
// connections on a specific port and if they are HTTP-01 challenge requests,
|
||||||
|
// serves the challenge response key authorization.
|
||||||
|
type challHTTPServer struct {
|
||||||
|
*http.Server
|
||||||
|
// mu protects challMap
|
||||||
|
mu sync.RWMutex
|
||||||
|
// challMap is a mapping from request path to response body.
|
||||||
|
challMap map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newChallHTTPServer(address string) *challHTTPServer {
|
||||||
|
challServer := &challHTTPServer{
|
||||||
|
Server: &http.Server{
|
||||||
|
Addr: address,
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 5 * time.Second,
|
||||||
|
},
|
||||||
|
challMap: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
challServer.Server.Handler = challServer
|
||||||
|
|
||||||
|
return challServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *challHTTPServer) Supported(chal *acme.Challenge) bool {
|
||||||
|
return chal.Type == "http-01"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *challHTTPServer) Provision(client *acme.Client, ident acme.AuthzID, chall *acme.Challenge) error {
|
||||||
|
path := client.HTTP01ChallengePath(chall.Token)
|
||||||
|
body, err := client.HTTP01ChallengeResponse(chall.Token)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate HTTP-01 challenge response for %v challenge %s token %s: %w",
|
||||||
|
ident, chall.URI, chall.Token, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
log.Printf("challHTTPServer: setting challenge response for %s", path)
|
||||||
|
c.challMap[path] = body
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *challHTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Printf("challHTTPServer: handling %s to %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
response, exists := c.challMap[r.URL.Path]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
w.Write([]byte(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *challHTTPServer) Shutdown() error {
|
||||||
|
log.Printf("challHTTPServer: shutting down")
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
return c.Server.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *challHTTPServer) Run() {
|
||||||
|
go func() {
|
||||||
|
if err := c.Server.ListenAndServe(); err != nil {
|
||||||
|
if !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Printf("challHTTPServer error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIssuance(t *testing.T, env *environment, challSrv challengeServer) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Bound the total issuance process by a timeout of 60 seconds.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Create a new ACME account.
|
||||||
|
client := env.client
|
||||||
|
acct, err := client.Register(ctx, &acme.Account{}, acme.AcceptTOS)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to register account: %v", err)
|
||||||
|
}
|
||||||
|
if acct.Status != acme.StatusValid {
|
||||||
|
t.Fatalf("expected new account status to be valid, got %v", acct.Status)
|
||||||
|
}
|
||||||
|
log.Printf("registered account: %s", acct.URI)
|
||||||
|
|
||||||
|
// Create a new order for some example identifiers
|
||||||
|
identifiers := []acme.AuthzID{
|
||||||
|
{
|
||||||
|
Type: "dns",
|
||||||
|
Value: "example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "dns",
|
||||||
|
Value: "www.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "ip",
|
||||||
|
Value: "127.0.0.1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
order, err := client.AuthorizeOrder(ctx, identifiers)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create order for %v: %v", identifiers, err)
|
||||||
|
}
|
||||||
|
if order.Status != acme.StatusPending {
|
||||||
|
t.Fatalf("expected new order status to be pending, got %v", order.Status)
|
||||||
|
}
|
||||||
|
orderURL := order.URI
|
||||||
|
log.Printf("created order: %v", orderURL)
|
||||||
|
|
||||||
|
// For each pending authz provision a supported challenge type's response
|
||||||
|
// with the test challenge server, and tell the ACME server to verify it.
|
||||||
|
for _, authzURL := range order.AuthzURLs {
|
||||||
|
authz, err := client.GetAuthorization(ctx, authzURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get order %s authorization %s: %v",
|
||||||
|
orderURL, authzURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if authz.Status != acme.StatusPending {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, challenge := range authz.Challenges {
|
||||||
|
if challenge.Status != acme.StatusPending || !challSrv.Supported(challenge) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := challSrv.Provision(client, authz.Identifier, challenge); err != nil {
|
||||||
|
t.Fatalf("failed to provision challenge %s: %v", challenge.URI, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.Accept(ctx, challenge)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to accept order %s challenge %s: %v",
|
||||||
|
orderURL, challenge.URI, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the order to become ready for finalization.
|
||||||
|
order, err = client.WaitOrder(ctx, order.URI)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to wait for order %s: %s", orderURL, err)
|
||||||
|
}
|
||||||
|
if order.Status != acme.StatusReady {
|
||||||
|
t.Fatalf("expected order %s status to be ready, got %v",
|
||||||
|
orderURL, order.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a certificate keypair and a CSR for the order identifiers.
|
||||||
|
certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to generate certificate key: %v", err)
|
||||||
|
}
|
||||||
|
var dnsNames []string
|
||||||
|
var ipAddresses []net.IP
|
||||||
|
for _, ident := range identifiers {
|
||||||
|
switch ident.Type {
|
||||||
|
case "dns":
|
||||||
|
dnsNames = append(dnsNames, ident.Value)
|
||||||
|
case "ip":
|
||||||
|
ipAddresses = append(ipAddresses, net.ParseIP(ident.Value))
|
||||||
|
default:
|
||||||
|
t.Fatalf("unsupported identifier type: %s", ident.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
csrDer, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
|
||||||
|
DNSNames: dnsNames,
|
||||||
|
IPAddresses: ipAddresses,
|
||||||
|
}, certKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create CSR: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize the order by creating a certificate with our CSR.
|
||||||
|
chain, _, err := client.CreateOrderCert(ctx, order.FinalizeURL, csrDer, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to finalize order %s with finalize URL %s: %v",
|
||||||
|
orderURL, order.FinalizeURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the chain into the leaf and any intermediates.
|
||||||
|
leaf := chain[0]
|
||||||
|
intermediatesDER := chain[1:]
|
||||||
|
leafCert, err := x509.ParseCertificate(leaf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse order %s leaf certificate: %v", orderURL, err)
|
||||||
|
}
|
||||||
|
intermediates := x509.NewCertPool()
|
||||||
|
for i, intermediateDER := range intermediatesDER {
|
||||||
|
intermediate, err := x509.ParseCertificate(intermediateDER)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse intermediate %d: %v", i, err)
|
||||||
|
}
|
||||||
|
intermediates.AddCert(intermediate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify there is a valid path from the leaf certificate to Pebble's
|
||||||
|
// issuing root using the provided intermediate certificates.
|
||||||
|
roots, err := env.RootCert()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get Pebble issuer root certs: %v", err)
|
||||||
|
}
|
||||||
|
paths, err := leafCert.Verify(x509.VerifyOptions{
|
||||||
|
Intermediates: intermediates,
|
||||||
|
Roots: roots,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to verify order %s leaf certificate: %v", orderURL, err)
|
||||||
|
}
|
||||||
|
log.Printf("verified %d path(s) from issued leaf certificate to Pebble root CA", len(paths))
|
||||||
|
|
||||||
|
// Also verify that the leaf cert is valid for each of the DNS names
|
||||||
|
// and IP addresses from our order's identifiers.
|
||||||
|
for _, name := range dnsNames {
|
||||||
|
if err := leafCert.VerifyHostname(name); err != nil {
|
||||||
|
t.Fatalf("failed to verify order %s leaf certificate for order DNS name %s: %v",
|
||||||
|
orderURL, name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ip := range ipAddresses {
|
||||||
|
if err := leafCert.VerifyHostname(ip.String()); err != nil {
|
||||||
|
t.Fatalf("failed to verify order %s leaf certificate for order IP address %s: %v",
|
||||||
|
orderURL, ip, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type environment struct {
|
||||||
|
config *environmentConfig
|
||||||
|
client *acme.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// RootCert returns the Pebble CA's primary issuing hierarchy root certificate.
|
||||||
|
// This is generated randomly at each startup and can be used to verify
|
||||||
|
// certificate chains issued by Pebble's ACME interface. Note that this
|
||||||
|
// is separate from the static root certificate used by the Pebble ACME
|
||||||
|
// HTTPS interface.
|
||||||
|
func (e *environment) RootCert() (*x509.CertPool, error) {
|
||||||
|
// NOTE: in the future we may want to consider the alternative chains
|
||||||
|
// returned as Link alternative headers.
|
||||||
|
rootURL := fmt.Sprintf("https://%s/roots/0", e.config.pebbleConfig.ManagementListenAddress)
|
||||||
|
resp, err := e.client.HTTPClient.Get(rootURL)
|
||||||
|
if err != nil || resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("failed to GET Pebble root CA from %s: %v", rootURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
roots := x509.NewCertPool()
|
||||||
|
rootPEM, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse Pebble root CA PEM: %v", err)
|
||||||
|
}
|
||||||
|
rootDERBlock, _ := pem.Decode(rootPEM)
|
||||||
|
rootCA, err := x509.ParseCertificate(rootDERBlock.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse Pebble root CA DER: %v", err)
|
||||||
|
}
|
||||||
|
roots.AddCert(rootCA)
|
||||||
|
|
||||||
|
return roots, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// environmentConfig describes the Pebble configuration, and configuration
|
||||||
|
// shared between pebble and pebble-challtestsrv.
|
||||||
|
type environmentConfig struct {
|
||||||
|
pebbleConfig
|
||||||
|
dnsPort uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultConfig returns an environmentConfig populated with default values.
|
||||||
|
// The provided pebbleDir is used to specify certificate/private key paths
|
||||||
|
// for the HTTPS ACME interface.
|
||||||
|
func defaultConfig(pebbleDir string) environmentConfig {
|
||||||
|
return environmentConfig{
|
||||||
|
pebbleConfig: pebbleConfig{
|
||||||
|
ListenAddress: fmt.Sprintf("127.0.0.1:%d", takeNextPort()),
|
||||||
|
ManagementListenAddress: fmt.Sprintf("127.0.0.1:%d", takeNextPort()),
|
||||||
|
HTTPPort: takeNextPort(),
|
||||||
|
TLSPort: takeNextPort(),
|
||||||
|
Certificate: fmt.Sprintf("%s/test/certs/localhost/cert.pem", pebbleDir),
|
||||||
|
PrivateKey: fmt.Sprintf("%s/test/certs/localhost/key.pem", pebbleDir),
|
||||||
|
OCSPResponderURL: "",
|
||||||
|
ExternalAccountBindingRequired: false,
|
||||||
|
ExternalAccountMACKeys: make(map[string]string),
|
||||||
|
DomainBlocklist: []string{"blocked-domain.example"},
|
||||||
|
Profiles: map[string]struct {
|
||||||
|
Description string
|
||||||
|
ValidityPeriod uint64
|
||||||
|
}{
|
||||||
|
"default": {
|
||||||
|
Description: "default profile",
|
||||||
|
ValidityPeriod: 3600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RetryAfter: struct {
|
||||||
|
Authz int
|
||||||
|
Order int
|
||||||
|
}{
|
||||||
|
3,
|
||||||
|
5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dnsPort: takeNextPort(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pebbleConfig matches the JSON structure of the Pebble configuration file.
|
||||||
|
type pebbleConfig struct {
|
||||||
|
ListenAddress string
|
||||||
|
ManagementListenAddress string
|
||||||
|
HTTPPort uint32
|
||||||
|
TLSPort uint32
|
||||||
|
Certificate string
|
||||||
|
PrivateKey string
|
||||||
|
OCSPResponderURL string
|
||||||
|
ExternalAccountBindingRequired bool
|
||||||
|
ExternalAccountMACKeys map[string]string
|
||||||
|
DomainBlocklist []string
|
||||||
|
Profiles map[string]struct {
|
||||||
|
Description string
|
||||||
|
ValidityPeriod uint64
|
||||||
|
}
|
||||||
|
RetryAfter struct {
|
||||||
|
Authz int
|
||||||
|
Order int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func takeNextPort() uint32 {
|
||||||
|
return nextPort.Add(1) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// startPebbleEnvironment is a test helper that spawns Pebble and Pebble
|
||||||
|
// challenge test server processes based on the provided environmentConfig. The
|
||||||
|
// processes will be torn down when the test ends.
|
||||||
|
func startPebbleEnvironment(t *testing.T, config *environmentConfig) environment {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var pebbleDir string
|
||||||
|
if *pebbleLocalDir != "" {
|
||||||
|
pebbleDir = *pebbleLocalDir
|
||||||
|
} else {
|
||||||
|
pebbleDir = fetchModule(t, "github.com/letsencrypt/pebble/v2", pebbleModVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
binDir := prepareBinaries(t, pebbleDir)
|
||||||
|
|
||||||
|
if config == nil {
|
||||||
|
cfg := defaultConfig(pebbleDir)
|
||||||
|
config = &cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
marshalConfig := struct {
|
||||||
|
Pebble pebbleConfig
|
||||||
|
}{
|
||||||
|
Pebble: config.pebbleConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
configData, err := json.Marshal(marshalConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshal config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile, err := os.CreateTemp("", "pebble-config-*.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp config file: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { os.Remove(configFile.Name()) })
|
||||||
|
|
||||||
|
if _, err := configFile.Write(configData); err != nil {
|
||||||
|
t.Fatalf("failed to write config file: %v", err)
|
||||||
|
}
|
||||||
|
configFile.Close()
|
||||||
|
|
||||||
|
log.Printf("pebble dir: %s", pebbleDir)
|
||||||
|
log.Printf("config file: %s", configFile.Name())
|
||||||
|
|
||||||
|
// Spawn the Pebble CA server. It answers ACME requests and performs
|
||||||
|
// outbound validations. We configure it to use a mock DNS server that
|
||||||
|
// always answers 127.0.0.1 for all A queries so that validation
|
||||||
|
// requests for any domain name will resolve to our local challenge
|
||||||
|
// server instances.
|
||||||
|
spawnServerProcess(t, binDir, "pebble", "-config", configFile.Name(),
|
||||||
|
"-dnsserver", fmt.Sprintf("127.0.0.1:%d", config.dnsPort),
|
||||||
|
"-strict")
|
||||||
|
|
||||||
|
// Spawn the Pebble challenge test server. We'll use it to mock DNS
|
||||||
|
// responses but disable all the other interfaces. We want to stand
|
||||||
|
// up our own challenge response servers for TLS-ALPN-01,
|
||||||
|
// etc.
|
||||||
|
// Note: we specify -defaultIPv6 "" so that no AAAA records are served.
|
||||||
|
// The LUCI CI runners have issues with IPv6 connectivity on localhost.
|
||||||
|
spawnServerProcess(t, binDir, "pebble-challtestsrv",
|
||||||
|
"-dns01", fmt.Sprintf(":%d", config.dnsPort),
|
||||||
|
"-defaultIPv6", "",
|
||||||
|
"-management", fmt.Sprintf(":%d", takeNextPort()),
|
||||||
|
"-doh", "",
|
||||||
|
"-http01", "",
|
||||||
|
"-tlsalpn01", "",
|
||||||
|
"-https01", "")
|
||||||
|
|
||||||
|
waitForServer(t, config.pebbleConfig.ListenAddress)
|
||||||
|
waitForServer(t, fmt.Sprintf("127.0.0.1:%d", config.dnsPort))
|
||||||
|
|
||||||
|
log.Printf("pebble environment ready")
|
||||||
|
|
||||||
|
// Construct a cert pool that contains the CA certificate used by the ACME
|
||||||
|
// interface's certificate chain. This is separate from the issuing
|
||||||
|
// hierarchy and is used for the ACME client to interact with the ACME
|
||||||
|
// interface without cert verification error.
|
||||||
|
caCertPath := filepath.Join(pebbleDir, "test/certs/pebble.minica.pem")
|
||||||
|
caCert, err := os.ReadFile(caCertPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read CA certificate %s: %v", caCertPath, err)
|
||||||
|
}
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
if !caCertPool.AppendCertsFromPEM(caCert) {
|
||||||
|
t.Fatalf("failed to parse CA certificate %s", caCertPath)
|
||||||
|
}
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
RootCAs: caCertPool,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an ACME account keypair/client and verify it can discover
|
||||||
|
// the Pebble server's ACME directory without error.
|
||||||
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to generate account key: %v", err)
|
||||||
|
}
|
||||||
|
client := &acme.Client{
|
||||||
|
Key: key,
|
||||||
|
HTTPClient: httpClient,
|
||||||
|
DirectoryURL: fmt.Sprintf("https://%s/dir", config.ListenAddress),
|
||||||
|
}
|
||||||
|
_, err = client.Discover(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to discover ACME directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return environment{
|
||||||
|
config: config,
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForServer(t *testing.T, addr string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
|
if conn, err := net.Dial("tcp", addr); err == nil {
|
||||||
|
conn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(time.Duration(i*100) * time.Millisecond)
|
||||||
|
}
|
||||||
|
t.Fatalf("failed to connect to %q after 20 tries", addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchModule fetches the module at the given version and returns the directory
|
||||||
|
// containing its source tree. It skips the test if fetching modules is not
|
||||||
|
// possible in this environment.
|
||||||
|
//
|
||||||
|
// Copied from the stdlib cryptotest.FetchModule and adapted to not rely on the
|
||||||
|
// stdlib internal testenv package.
|
||||||
|
func fetchModule(t *testing.T, module, version string) string {
|
||||||
|
// If the default GOMODCACHE doesn't exist, use a temporary directory
|
||||||
|
// instead. (For example, run.bash sets GOPATH=/nonexist-gopath.)
|
||||||
|
out, err := exec.Command("go", "env", "GOMODCACHE").Output()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("go env GOMODCACHE: %v\n%s", err, out)
|
||||||
|
if ee, ok := err.(*exec.ExitError); ok {
|
||||||
|
t.Logf("%s", ee.Stderr)
|
||||||
|
}
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
modcacheOk := false
|
||||||
|
if gomodcache := string(bytes.TrimSpace(out)); gomodcache != "" {
|
||||||
|
if _, err := os.Stat(gomodcache); err == nil {
|
||||||
|
modcacheOk = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !modcacheOk {
|
||||||
|
t.Setenv("GOMODCACHE", t.TempDir())
|
||||||
|
// Allow t.TempDir() to clean up subdirectories.
|
||||||
|
t.Setenv("GOFLAGS", os.Getenv("GOFLAGS")+" -modcacherw")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("fetching %s@%s\n", module, version)
|
||||||
|
|
||||||
|
output, err := exec.Command("go", "mod", "download", "-json", module+"@"+version).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to download %s@%s: %s\n%s\n", module, version, err, output)
|
||||||
|
}
|
||||||
|
var j struct {
|
||||||
|
Dir string
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(output, &j); err != nil {
|
||||||
|
t.Fatalf("failed to parse 'go mod download': %s\n%s\n", err, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
return j.Dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareBinaries(t *testing.T, pebbleDir string) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// We don't want to build in the module cache dir, which might not be
|
||||||
|
// writable or to pollute the user's clone with binaries if pebbleLocalDir
|
||||||
|
//is used.
|
||||||
|
binDir := t.TempDir()
|
||||||
|
|
||||||
|
build := func(cmd string) {
|
||||||
|
log.Printf("building %s", cmd)
|
||||||
|
buildCmd := exec.Command(
|
||||||
|
"go",
|
||||||
|
"build", "-o", filepath.Join(binDir, cmd), "-mod", "mod", "./cmd/"+cmd)
|
||||||
|
buildCmd.Dir = pebbleDir
|
||||||
|
output, err := buildCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to build %s: %s\n%s\n", cmd, err, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
build("pebble")
|
||||||
|
build("pebble-challtestsrv")
|
||||||
|
|
||||||
|
return binDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func spawnServerProcess(t *testing.T, dir string, cmd string, args ...string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
|
||||||
|
cmdInstance := exec.Command("./"+cmd, args...)
|
||||||
|
cmdInstance.Dir = dir
|
||||||
|
cmdInstance.Stdout = &stdout
|
||||||
|
cmdInstance.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmdInstance.Start(); err != nil {
|
||||||
|
t.Fatalf("failed to start %s: %v", cmd, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
cmdInstance.Process.Kill()
|
||||||
|
cmdInstance.Wait()
|
||||||
|
|
||||||
|
if t.Failed() || testing.Verbose() {
|
||||||
|
t.Logf("=== %s output ===", cmd)
|
||||||
|
if stdout.Len() > 0 {
|
||||||
|
t.Logf("stdout:\n%s", strings.TrimSpace(stdout.String()))
|
||||||
|
}
|
||||||
|
if stderr.Len() > 0 {
|
||||||
|
t.Logf("stderr:\n%s", strings.TrimSpace(stderr.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
+1
-1
@@ -619,7 +619,7 @@ func (*certOptKey) privateCertOpt() {}
|
|||||||
//
|
//
|
||||||
// In TLS ChallengeCert methods, the template is also used as parent,
|
// In TLS ChallengeCert methods, the template is also used as parent,
|
||||||
// resulting in a self-signed certificate.
|
// resulting in a self-signed certificate.
|
||||||
// The DNSNames field of t is always overwritten for tls-sni challenge certs.
|
// The DNSNames or IPAddresses fields of t are always overwritten for tls-alpn challenge certs.
|
||||||
func WithTemplate(t *x509.Certificate) CertOption {
|
func WithTemplate(t *x509.Certificate) CertOption {
|
||||||
return (*certOptTemplate)(t)
|
return (*certOptTemplate)(t)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
|
|
||||||
// XOF defines the interface to hash functions that
|
// XOF defines the interface to hash functions that
|
||||||
// support arbitrary-length output.
|
// support arbitrary-length output.
|
||||||
|
//
|
||||||
|
// New callers should prefer the standard library [hash.XOF].
|
||||||
type XOF interface {
|
type XOF interface {
|
||||||
// Write absorbs more data into the hash's state. It panics if called
|
// Write absorbs more data into the hash's state. It panics if called
|
||||||
// after Read.
|
// after Read.
|
||||||
@@ -47,6 +49,8 @@ const maxOutputLength = (1 << 32) * 64
|
|||||||
//
|
//
|
||||||
// A non-nil key turns the hash into a MAC. The key must between
|
// A non-nil key turns the hash into a MAC. The key must between
|
||||||
// zero and 32 bytes long.
|
// zero and 32 bytes long.
|
||||||
|
//
|
||||||
|
// The result can be safely interface-upgraded to [hash.XOF].
|
||||||
func NewXOF(size uint32, key []byte) (XOF, error) {
|
func NewXOF(size uint32, key []byte) (XOF, error) {
|
||||||
if len(key) > Size {
|
if len(key) > Size {
|
||||||
return nil, errKeySize
|
return nil, errKeySize
|
||||||
@@ -93,6 +97,10 @@ func (x *xof) Clone() XOF {
|
|||||||
return &clone
|
return &clone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *xof) BlockSize() int {
|
||||||
|
return x.d.BlockSize()
|
||||||
|
}
|
||||||
|
|
||||||
func (x *xof) Reset() {
|
func (x *xof) Reset() {
|
||||||
x.cfg[0] = byte(Size)
|
x.cfg[0] = byte(Size)
|
||||||
binary.LittleEndian.PutUint32(x.cfg[4:], uint32(Size)) // leaf length
|
binary.LittleEndian.PutUint32(x.cfg[4:], uint32(Size)) // leaf length
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright 2025 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build go1.25
|
||||||
|
|
||||||
|
package blake2b
|
||||||
|
|
||||||
|
import "hash"
|
||||||
|
|
||||||
|
var _ hash.XOF = (*xof)(nil)
|
||||||
@@ -3,9 +3,9 @@ module golang.org/x/crypto
|
|||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/net v0.21.0 // tagx:ignore
|
golang.org/x/net v0.42.0 // tagx:ignore
|
||||||
golang.org/x/sys v0.33.0
|
golang.org/x/sys v0.35.0
|
||||||
golang.org/x/term v0.32.0
|
golang.org/x/term v0.34.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/text v0.26.0 // indirect
|
require golang.org/x/text v0.28.0 // indirect
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
|||||||
+17
-9
@@ -233,7 +233,11 @@ func parseCert(in []byte, privAlgo string) (*Certificate, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// The Type() function is intended to return only certificate key types, but
|
||||||
|
// we use certKeyAlgoNames anyway for safety, to match [Certificate.Type].
|
||||||
|
if _, ok := certKeyAlgoNames[k.Type()]; ok {
|
||||||
|
return nil, fmt.Errorf("ssh: the signature key type %q is invalid for certificates", k.Type())
|
||||||
|
}
|
||||||
c.SignatureKey = k
|
c.SignatureKey = k
|
||||||
c.Signature, rest, ok = parseSignatureBody(g.Signature)
|
c.Signature, rest, ok = parseSignatureBody(g.Signature)
|
||||||
if !ok || len(rest) > 0 {
|
if !ok || len(rest) > 0 {
|
||||||
@@ -301,16 +305,13 @@ type CertChecker struct {
|
|||||||
SupportedCriticalOptions []string
|
SupportedCriticalOptions []string
|
||||||
|
|
||||||
// IsUserAuthority should return true if the key is recognized as an
|
// IsUserAuthority should return true if the key is recognized as an
|
||||||
// authority for the given user certificate. This allows for
|
// authority for user certificate. This must be set if this CertChecker
|
||||||
// certificates to be signed by other certificates. This must be set
|
// will be checking user certificates.
|
||||||
// if this CertChecker will be checking user certificates.
|
|
||||||
IsUserAuthority func(auth PublicKey) bool
|
IsUserAuthority func(auth PublicKey) bool
|
||||||
|
|
||||||
// IsHostAuthority should report whether the key is recognized as
|
// IsHostAuthority should report whether the key is recognized as
|
||||||
// an authority for this host. This allows for certificates to be
|
// an authority for this host. This must be set if this CertChecker
|
||||||
// signed by other keys, and for those other keys to only be valid
|
// will be checking host certificates.
|
||||||
// signers for particular hostnames. This must be set if this
|
|
||||||
// CertChecker will be checking host certificates.
|
|
||||||
IsHostAuthority func(auth PublicKey, address string) bool
|
IsHostAuthority func(auth PublicKey, address string) bool
|
||||||
|
|
||||||
// Clock is used for verifying time stamps. If nil, time.Now
|
// Clock is used for verifying time stamps. If nil, time.Now
|
||||||
@@ -447,12 +448,19 @@ func (c *CertChecker) CheckCert(principal string, cert *Certificate) error {
|
|||||||
// SignCert signs the certificate with an authority, setting the Nonce,
|
// SignCert signs the certificate with an authority, setting the Nonce,
|
||||||
// SignatureKey, and Signature fields. If the authority implements the
|
// SignatureKey, and Signature fields. If the authority implements the
|
||||||
// MultiAlgorithmSigner interface the first algorithm in the list is used. This
|
// MultiAlgorithmSigner interface the first algorithm in the list is used. This
|
||||||
// is useful if you want to sign with a specific algorithm.
|
// is useful if you want to sign with a specific algorithm. As specified in
|
||||||
|
// [SSH-CERTS], Section 2.1.1, authority can't be a [Certificate].
|
||||||
func (c *Certificate) SignCert(rand io.Reader, authority Signer) error {
|
func (c *Certificate) SignCert(rand io.Reader, authority Signer) error {
|
||||||
c.Nonce = make([]byte, 32)
|
c.Nonce = make([]byte, 32)
|
||||||
if _, err := io.ReadFull(rand, c.Nonce); err != nil {
|
if _, err := io.ReadFull(rand, c.Nonce); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// The Type() function is intended to return only certificate key types, but
|
||||||
|
// we use certKeyAlgoNames anyway for safety, to match [Certificate.Type].
|
||||||
|
if _, ok := certKeyAlgoNames[authority.PublicKey().Type()]; ok {
|
||||||
|
return fmt.Errorf("ssh: certificates cannot be used as authority (public key type %q)",
|
||||||
|
authority.PublicKey().Type())
|
||||||
|
}
|
||||||
c.SignatureKey = authority.PublicKey()
|
c.SignatureKey = authority.PublicKey()
|
||||||
|
|
||||||
if v, ok := authority.(MultiAlgorithmSigner); ok {
|
if v, ok := authority.(MultiAlgorithmSigner); ok {
|
||||||
|
|||||||
@@ -404,3 +404,32 @@ func TestCertSignWithMultiAlgorithmSigner(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCertSignWithCertificate(t *testing.T) {
|
||||||
|
cert := &Certificate{
|
||||||
|
Key: testPublicKeys["rsa"],
|
||||||
|
ValidBefore: CertTimeInfinity,
|
||||||
|
CertType: UserCert,
|
||||||
|
}
|
||||||
|
if err := cert.SignCert(rand.Reader, testSigners["ecdsa"]); err != nil {
|
||||||
|
t.Fatalf("SignCert: %v", err)
|
||||||
|
}
|
||||||
|
signer, err := NewSignerWithAlgorithms(testSigners["rsa"].(AlgorithmSigner), []string{KeyAlgoRSASHA256})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
certSigner, err := NewCertSigner(cert, signer)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewCertSigner: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cert1 := &Certificate{
|
||||||
|
Key: testPublicKeys["ecdsa"],
|
||||||
|
ValidBefore: CertTimeInfinity,
|
||||||
|
CertType: UserCert,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cert1.SignCert(rand.Reader, certSigner); err == nil {
|
||||||
|
t.Fatal("successfully signed a certificate using another certificate, it is expected to fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -289,7 +289,7 @@ func pickSignatureAlgorithm(signer Signer, extensions map[string][]byte) (MultiA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
algo, err := findCommon("public key signature algorithm", keyAlgos, serverAlgos)
|
algo, err := findCommon("public key signature algorithm", keyAlgos, serverAlgos, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If there is no overlap, return the fallback algorithm to support
|
// If there is no overlap, return the fallback algorithm to support
|
||||||
// servers that fail to list all supported algorithms.
|
// servers that fail to list all supported algorithms.
|
||||||
|
|||||||
+35
-10
@@ -336,7 +336,7 @@ func parseError(tag uint8) error {
|
|||||||
return fmt.Errorf("ssh: parse error in message type %d", tag)
|
return fmt.Errorf("ssh: parse error in message type %d", tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
func findCommon(what string, client []string, server []string) (common string, err error) {
|
func findCommon(what string, client []string, server []string, isClient bool) (string, error) {
|
||||||
for _, c := range client {
|
for _, c := range client {
|
||||||
for _, s := range server {
|
for _, s := range server {
|
||||||
if c == s {
|
if c == s {
|
||||||
@@ -344,7 +344,32 @@ func findCommon(what string, client []string, server []string) (common string, e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("ssh: no common algorithm for %s; client offered: %v, server offered: %v", what, client, server)
|
err := &AlgorithmNegotiationError{
|
||||||
|
What: what,
|
||||||
|
}
|
||||||
|
if isClient {
|
||||||
|
err.SupportedAlgorithms = client
|
||||||
|
err.RequestedAlgorithms = server
|
||||||
|
} else {
|
||||||
|
err.SupportedAlgorithms = server
|
||||||
|
err.RequestedAlgorithms = client
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlgorithmNegotiationError defines the error returned if the client and the
|
||||||
|
// server cannot agree on an algorithm for key exchange, host key, cipher, MAC.
|
||||||
|
type AlgorithmNegotiationError struct {
|
||||||
|
What string
|
||||||
|
// RequestedAlgorithms lists the algorithms supported by the peer.
|
||||||
|
RequestedAlgorithms []string
|
||||||
|
// SupportedAlgorithms lists the algorithms supported on our side.
|
||||||
|
SupportedAlgorithms []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AlgorithmNegotiationError) Error() string {
|
||||||
|
return fmt.Sprintf("ssh: no common algorithm for %s; we offered: %v, peer offered: %v",
|
||||||
|
a.What, a.SupportedAlgorithms, a.RequestedAlgorithms)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DirectionAlgorithms defines the algorithms negotiated in one direction
|
// DirectionAlgorithms defines the algorithms negotiated in one direction
|
||||||
@@ -379,12 +404,12 @@ var aeadCiphers = map[string]bool{
|
|||||||
func findAgreedAlgorithms(isClient bool, clientKexInit, serverKexInit *kexInitMsg) (algs *NegotiatedAlgorithms, err error) {
|
func findAgreedAlgorithms(isClient bool, clientKexInit, serverKexInit *kexInitMsg) (algs *NegotiatedAlgorithms, err error) {
|
||||||
result := &NegotiatedAlgorithms{}
|
result := &NegotiatedAlgorithms{}
|
||||||
|
|
||||||
result.KeyExchange, err = findCommon("key exchange", clientKexInit.KexAlgos, serverKexInit.KexAlgos)
|
result.KeyExchange, err = findCommon("key exchange", clientKexInit.KexAlgos, serverKexInit.KexAlgos, isClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result.HostKey, err = findCommon("host key", clientKexInit.ServerHostKeyAlgos, serverKexInit.ServerHostKeyAlgos)
|
result.HostKey, err = findCommon("host key", clientKexInit.ServerHostKeyAlgos, serverKexInit.ServerHostKeyAlgos, isClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -394,36 +419,36 @@ func findAgreedAlgorithms(isClient bool, clientKexInit, serverKexInit *kexInitMs
|
|||||||
ctos, stoc = stoc, ctos
|
ctos, stoc = stoc, ctos
|
||||||
}
|
}
|
||||||
|
|
||||||
ctos.Cipher, err = findCommon("client to server cipher", clientKexInit.CiphersClientServer, serverKexInit.CiphersClientServer)
|
ctos.Cipher, err = findCommon("client to server cipher", clientKexInit.CiphersClientServer, serverKexInit.CiphersClientServer, isClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stoc.Cipher, err = findCommon("server to client cipher", clientKexInit.CiphersServerClient, serverKexInit.CiphersServerClient)
|
stoc.Cipher, err = findCommon("server to client cipher", clientKexInit.CiphersServerClient, serverKexInit.CiphersServerClient, isClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !aeadCiphers[ctos.Cipher] {
|
if !aeadCiphers[ctos.Cipher] {
|
||||||
ctos.MAC, err = findCommon("client to server MAC", clientKexInit.MACsClientServer, serverKexInit.MACsClientServer)
|
ctos.MAC, err = findCommon("client to server MAC", clientKexInit.MACsClientServer, serverKexInit.MACsClientServer, isClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !aeadCiphers[stoc.Cipher] {
|
if !aeadCiphers[stoc.Cipher] {
|
||||||
stoc.MAC, err = findCommon("server to client MAC", clientKexInit.MACsServerClient, serverKexInit.MACsServerClient)
|
stoc.MAC, err = findCommon("server to client MAC", clientKexInit.MACsServerClient, serverKexInit.MACsServerClient, isClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctos.compression, err = findCommon("client to server compression", clientKexInit.CompressionClientServer, serverKexInit.CompressionClientServer)
|
ctos.compression, err = findCommon("client to server compression", clientKexInit.CompressionClientServer, serverKexInit.CompressionClientServer, isClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stoc.compression, err = findCommon("server to client compression", clientKexInit.CompressionServerClient, serverKexInit.CompressionServerClient)
|
stoc.compression, err = findCommon("server to client compression", clientKexInit.CompressionServerClient, serverKexInit.CompressionServerClient, isClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ References:
|
|||||||
[PROTOCOL]: https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL?rev=HEAD
|
[PROTOCOL]: https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL?rev=HEAD
|
||||||
[PROTOCOL.certkeys]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys?rev=HEAD
|
[PROTOCOL.certkeys]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys?rev=HEAD
|
||||||
[SSH-PARAMETERS]: http://www.iana.org/assignments/ssh-parameters/ssh-parameters.xml#ssh-parameters-1
|
[SSH-PARAMETERS]: http://www.iana.org/assignments/ssh-parameters/ssh-parameters.xml#ssh-parameters-1
|
||||||
|
[SSH-CERTS]: https://datatracker.ietf.org/doc/html/draft-miller-ssh-cert-01
|
||||||
|
|
||||||
This package does not fall under the stability promise of the Go language itself,
|
This package does not fall under the stability promise of the Go language itself,
|
||||||
so its API may be changed when pressing needs arise.
|
so its API may be changed when pressing needs arise.
|
||||||
|
|||||||
@@ -1294,3 +1294,73 @@ func TestNegotiatedAlgorithms(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAlgorithmNegotiationError(t *testing.T) {
|
||||||
|
c1, c2, err := netPipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("netPipe: %v", err)
|
||||||
|
}
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
serverConf := &ServerConfig{
|
||||||
|
Config: Config{
|
||||||
|
Ciphers: []string{CipherAES128CTR, CipherAES256CTR},
|
||||||
|
},
|
||||||
|
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
|
||||||
|
return &Permissions{}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
serverConf.AddHostKey(testSigners["rsa"])
|
||||||
|
|
||||||
|
srvErrCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
_, _, _, err := NewServerConn(c1, serverConf)
|
||||||
|
srvErrCh <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
clientConf := &ClientConfig{
|
||||||
|
Config: Config{
|
||||||
|
Ciphers: []string{CipherAES128GCM, CipherAES256GCM},
|
||||||
|
},
|
||||||
|
User: "test",
|
||||||
|
Auth: []AuthMethod{Password("testpw")},
|
||||||
|
HostKeyCallback: FixedHostKey(testSigners["rsa"].PublicKey()),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, err = NewClientConn(c2, "", clientConf)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("client connection succeded expected algorithm negotiation error")
|
||||||
|
}
|
||||||
|
var negotiationError *AlgorithmNegotiationError
|
||||||
|
if !errors.As(err, &negotiationError) {
|
||||||
|
t.Fatalf("expected algorithm negotiation error, got %v", err)
|
||||||
|
}
|
||||||
|
expectedErrorString := fmt.Sprintf("ssh: handshake failed: ssh: no common algorithm for client to server cipher; we offered: %v, peer offered: %v",
|
||||||
|
clientConf.Ciphers, serverConf.Ciphers)
|
||||||
|
if err.Error() != expectedErrorString {
|
||||||
|
t.Fatalf("expected error string %q, got %q", expectedErrorString, err.Error())
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(negotiationError.RequestedAlgorithms, serverConf.Ciphers) {
|
||||||
|
t.Fatalf("expected requested algorithms %v, got %v", serverConf.Ciphers, negotiationError.RequestedAlgorithms)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(negotiationError.SupportedAlgorithms, clientConf.Ciphers) {
|
||||||
|
t.Fatalf("expected supported algorithms %v, got %v", clientConf.Ciphers, negotiationError.SupportedAlgorithms)
|
||||||
|
}
|
||||||
|
err = <-srvErrCh
|
||||||
|
negotiationError = nil
|
||||||
|
if !errors.As(err, &negotiationError) {
|
||||||
|
t.Fatalf("expected algorithm negotiation error, got %v", err)
|
||||||
|
}
|
||||||
|
expectedErrorString = fmt.Sprintf("ssh: no common algorithm for client to server cipher; we offered: %v, peer offered: %v",
|
||||||
|
serverConf.Ciphers, clientConf.Ciphers)
|
||||||
|
if err.Error() != expectedErrorString {
|
||||||
|
t.Fatalf("expected error string %q, got %q", expectedErrorString, err.Error())
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(negotiationError.RequestedAlgorithms, clientConf.Ciphers) {
|
||||||
|
t.Fatalf("expected requested algorithms %v, got %v", clientConf.Ciphers, negotiationError.RequestedAlgorithms)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(negotiationError.SupportedAlgorithms, serverConf.Ciphers) {
|
||||||
|
t.Fatalf("expected supported algorithms %v, got %v", serverConf.Ciphers, negotiationError.SupportedAlgorithms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -273,7 +273,7 @@ func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []str
|
|||||||
return nil, "", nil, nil, errors.New("ssh: no key found")
|
return nil, "", nil, nil, errors.New("ssh: no key found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParsePublicKey parses an SSH public key formatted for use in
|
// ParsePublicKey parses an SSH public key or certificate formatted for use in
|
||||||
// the SSH wire protocol according to RFC 4253, section 6.6.
|
// the SSH wire protocol according to RFC 4253, section 6.6.
|
||||||
func ParsePublicKey(in []byte) (out PublicKey, err error) {
|
func ParsePublicKey(in []byte) (out PublicKey, err error) {
|
||||||
algo, in, ok := parseString(in)
|
algo, in, ok := parseString(in)
|
||||||
|
|||||||
@@ -810,3 +810,30 @@ func TestCryptoPublicKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseCertWithCertSignatureKey(t *testing.T) {
|
||||||
|
certBytes := []byte(`-----BEGIN SSH CERTIFICATE-----
|
||||||
|
AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIPSp27hvNSB0
|
||||||
|
IotJnVhjC4zxNgNS8BHlUCxD0VJi4D/eAAAAIIJMi1e5qfx+IFuKD/p/Ssqcb3os
|
||||||
|
CpOw/4wBs1pQ53zwAAAAAAAAAAEAAAACAAAAAAAAABMAAAAPZm9vLmV4YW1wbGUu
|
||||||
|
Y29tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAT0AAAAgc3NoLWVkMjU1
|
||||||
|
MTktY2VydC12MDFAb3BlbnNzaC5jb20AAAAg+sNYhCO35mQT1UBMpmMk8ey+culd
|
||||||
|
IU8vBlPEl4B07swAAAAggiv+RLnboS4znGCVl/n1jDg2uD0h15tW4s/04eS2mLQA
|
||||||
|
AAAAAAAAAQAAAAIAAAAAAAAAEwAAAA9mb28uZXhhbXBsZS5jb20AAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACCV2wETgLKL
|
||||||
|
Kt0bRl3YUnd/ZYSlq0xJMbn4Jj3cdPWykQAAAFMAAAALc3NoLWVkMjU1MTkAAABA
|
||||||
|
WOdbRGEzyRAhiIK227CLUQD5caXYMV8FvSIB7toEE2M/8HnWdG9H3Rsg/v3unruQ
|
||||||
|
JrQldnuPJNe7KOP2+zvUDgAAAFMAAAALc3NoLWVkMjU1MTkAAABAm3bIPp85ZpIe
|
||||||
|
D+izJcUqlcAOri7HO8bULFNHT6LVegvB06xQ5TLwMlrxWUF4cafl1tSe8JQck4a6
|
||||||
|
cLYUOHfQDw==
|
||||||
|
-----END SSH CERTIFICATE-----
|
||||||
|
`)
|
||||||
|
block, _ := pem.Decode(certBytes)
|
||||||
|
if block == nil {
|
||||||
|
t.Fatal("invalid test certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := ParsePublicKey(block.Bytes); err == nil {
|
||||||
|
t.Fatal("parsing an SSH certificate using another certificate as signature key succeeded; expected failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
+753
-4098
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
|||||||
|
// Copyright 2025 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package fallback
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBundle(t *testing.T) {
|
||||||
|
for i, unparsed := range unparsedCertificates {
|
||||||
|
cert, err := x509.ParseCertificate(rawCerts[unparsed.certStartOff : unparsed.certStartOff+unparsed.certLength])
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("ParseCertificate(unparsedCertificates[%v]) unexpected error: %v", i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if unparsed.cn != cert.Subject.String() {
|
||||||
|
t.Errorf("unparsedCertificates[%v].cn = %q; want = %q", i, unparsed.cn, cert.Subject.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
sum := sha256.Sum256(cert.Raw)
|
||||||
|
sumHex := hex.EncodeToString(sum[:])
|
||||||
|
if sumHex != unparsed.sha256Hash {
|
||||||
|
t.Errorf("unparsedCertificates[%v].sha256Hash = %q; want = %q", i, unparsed.sha256Hash, sumHex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,11 +20,14 @@ package fallback
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed bundle.der
|
||||||
|
var rawCerts []byte
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
x509.SetFallbackRoots(newFallbackCertPool())
|
x509.SetFallbackRoots(newFallbackCertPool())
|
||||||
}
|
}
|
||||||
@@ -49,9 +52,10 @@ func newFallbackCertPool() *x509.CertPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type unparsedCertificate struct {
|
type unparsedCertificate struct {
|
||||||
cn string
|
cn string
|
||||||
sha256Hash string
|
sha256Hash string
|
||||||
pem string
|
certStartOff int
|
||||||
|
certLength int
|
||||||
|
|
||||||
// possible constraints
|
// possible constraints
|
||||||
distrustAfter string
|
distrustAfter string
|
||||||
@@ -63,19 +67,9 @@ type parsedCertificate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func mustParse(unparsedCerts []unparsedCertificate) []parsedCertificate {
|
func mustParse(unparsedCerts []unparsedCertificate) []parsedCertificate {
|
||||||
var b []parsedCertificate
|
b := make([]parsedCertificate, 0, len(unparsedCerts))
|
||||||
for _, unparsed := range unparsedCerts {
|
for _, unparsed := range unparsedCerts {
|
||||||
block, rest := pem.Decode([]byte(unparsed.pem))
|
cert, err := x509.ParseCertificate(rawCerts[unparsed.certStartOff : unparsed.certStartOff+unparsed.certLength])
|
||||||
if block == nil {
|
|
||||||
panic(fmt.Sprintf("unexpected nil PEM block for %q", unparsed.cn))
|
|
||||||
}
|
|
||||||
if len(rest) != 0 {
|
|
||||||
panic(fmt.Sprintf("unexpected trailing data in PEM for %q", unparsed.cn))
|
|
||||||
}
|
|
||||||
if block.Type != "CERTIFICATE" {
|
|
||||||
panic(fmt.Sprintf("unexpected PEM block type for %q: %s", unparsed.cn, block.Type))
|
|
||||||
}
|
|
||||||
cert, err := x509.ParseCertificate(block.Bytes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/pem"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/format"
|
"go/format"
|
||||||
@@ -37,6 +36,7 @@ var (
|
|||||||
certDataURL = flag.String("certdata-url", "https://hg.mozilla.org/mozilla-central/raw-file/tip/security/nss/lib/ckfw/builtins/certdata.txt", "URL to the raw certdata.txt file to parse (certdata-path overrides this, if provided)")
|
certDataURL = flag.String("certdata-url", "https://hg.mozilla.org/mozilla-central/raw-file/tip/security/nss/lib/ckfw/builtins/certdata.txt", "URL to the raw certdata.txt file to parse (certdata-path overrides this, if provided)")
|
||||||
certDataPath = flag.String("certdata-path", "", "Path to the NSS certdata.txt file to parse (this overrides certdata-url, if provided)")
|
certDataPath = flag.String("certdata-path", "", "Path to the NSS certdata.txt file to parse (this overrides certdata-url, if provided)")
|
||||||
output = flag.String("output", "fallback/bundle.go", "Path to file to write output to")
|
output = flag.String("output", "fallback/bundle.go", "Path to file to write output to")
|
||||||
|
derOutput = flag.String("deroutput", "fallback/bundle.der", "Path to file to write output to (DER certificate bundle)")
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -92,8 +92,9 @@ func main() {
|
|||||||
return subjI < subjJ
|
return subjI < subjJ
|
||||||
})
|
})
|
||||||
|
|
||||||
b := new(bytes.Buffer)
|
rawCertsData := new(bytes.Buffer)
|
||||||
b.WriteString(tmpl)
|
goSrcOut := new(bytes.Buffer)
|
||||||
|
goSrcOut.WriteString(tmpl)
|
||||||
for _, c := range certs {
|
for _, c := range certs {
|
||||||
var constraints []string
|
var constraints []string
|
||||||
var skip bool
|
var skip bool
|
||||||
@@ -110,19 +111,24 @@ func main() {
|
|||||||
if skip {
|
if skip {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fmt.Fprintf(b, "{\ncn: %q,\nsha256Hash: \"%x\",\npem: `%s`,\n",
|
|
||||||
|
off := rawCertsData.Len()
|
||||||
|
rawCertsData.Write(c.X509.Raw)
|
||||||
|
|
||||||
|
fmt.Fprintf(goSrcOut, "{\ncn: %q,\nsha256Hash: \"%x\",\ncertStartOff: %v,\ncertLength: %v,\n",
|
||||||
c.X509.Subject.String(),
|
c.X509.Subject.String(),
|
||||||
sha256.Sum256(c.X509.Raw),
|
sha256.Sum256(c.X509.Raw),
|
||||||
string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.X509.Raw})),
|
off,
|
||||||
|
len(c.X509.Raw),
|
||||||
)
|
)
|
||||||
for _, constraint := range constraints {
|
for _, constraint := range constraints {
|
||||||
fmt.Fprintln(b, constraint)
|
fmt.Fprintln(goSrcOut, constraint)
|
||||||
}
|
}
|
||||||
fmt.Fprintln(b, "},")
|
fmt.Fprintln(goSrcOut, "},")
|
||||||
}
|
}
|
||||||
fmt.Fprintln(b, "}")
|
fmt.Fprintln(goSrcOut, "}")
|
||||||
|
|
||||||
formatted, err := format.Source(b.Bytes())
|
formatted, err := format.Source(goSrcOut.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to format source: %s", err)
|
log.Fatalf("failed to format source: %s", err)
|
||||||
}
|
}
|
||||||
@@ -130,4 +136,8 @@ func main() {
|
|||||||
if err := os.WriteFile(*output, formatted, 0644); err != nil {
|
if err := os.WriteFile(*output, formatted, 0644); err != nil {
|
||||||
log.Fatalf("failed to write to %q: %s", *output, err)
|
log.Fatalf("failed to write to %q: %s", *output, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(*derOutput, rawCertsData.Bytes(), 0644); err != nil {
|
||||||
|
log.Fatalf("failed to write to %q: %s", *output, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user